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('la liste affiche le badge exemple des programmes seedés', ( tester, ) async { final programRepository = _FakeProgramRepository() ..programs.add( Program( metadata: _metadata('program-1'), name: 'Fondations basket - 45 min', defaultRestSeconds: 45, isExample: true, exercises: [ ProgramExercise( metadata: _metadata('program-exercise-1'), programId: 'program-1', position: 0, exerciseNameSnapshot: 'Tirs', availableTimeSnapshot: false, availableRepsSnapshot: true, availableScoreSnapshot: false, setsCount: 3, timeEnabled: false, repsEnabled: true, scoreEnabled: false, targetReps: 10, ), ], ), ); final exerciseRepository = _FakeExerciseRepository(); await tester.pumpWidget( MaterialApp( home: ProgramListScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); await tester.pump(); await tester.pump(); expect(find.text('Fondations basket - 45 min'), findsOneWidget); expect(find.text('Exemple'), findsOneWidget); expect(find.byTooltip('Actions'), findsOneWidget); }); testWidgets('la liste programmes vide conserve le message dédié', ( tester, ) async { final programRepository = _FakeProgramRepository(); final exerciseRepository = _FakeExerciseRepository(); await tester.pumpWidget( MaterialApp( home: ProgramListScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); await tester.pump(); await tester.pump(); expect(find.text('Aucun programme'), findsOneWidget); expect( find.text('Assemble quelques exercices pour préparer une séance.'), findsOneWidget, ); }); testWidgets('un programme sans exercice ne peut pas être enregistré', ( tester, ) async { final programRepository = _FakeProgramRepository(); final exerciseRepository = _FakeExerciseRepository(); await tester.pumpWidget( MaterialApp( home: ProgramFormScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); expect( find.text('Ajoute au moins un exercice pour enregistrer ce programme.'), findsOneWidget, ); expect( tester .widget( find.widgetWithText(FilledButton, 'Enregistrer'), ) .onPressed, isNull, ); await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Vide'); await tester.tap(find.text('Enregistrer')); await tester.pump(); expect(programRepository.saved, isEmpty); }); testWidgets('filtre les programmes par tag et peut effacer les filtres', ( tester, ) async { final programRepository = _FakeProgramRepository() ..programs.addAll([ Program( metadata: _metadata('program-1'), name: 'Programme match', defaultRestSeconds: 45, tags: const ['match', 'intense'], ), Program( metadata: _metadata('program-2'), name: 'Programme routine', defaultRestSeconds: 30, tags: const ['routine'], ), ]); final exerciseRepository = _FakeExerciseRepository(); await tester.pumpWidget( MaterialApp( home: ProgramListScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); await tester.pump(); await tester.pump(); expect(find.widgetWithText(FilterChip, 'match'), findsOneWidget); await tester.tap(find.widgetWithText(FilterChip, 'match')); await tester.pump(); expect(find.text('Programme match'), findsOneWidget); expect(find.text('Programme routine'), findsNothing); expect(find.text('Effacer les filtres'), findsOneWidget); await tester.tap(find.text('Effacer les filtres')); await tester.pump(); expect(find.text('Programme match'), findsOneWidget); expect(find.text('Programme routine'), findsOneWidget); }); testWidgets('dupliquer un programme ouvre la copie en édition', ( tester, ) async { final programRepository = _FakeProgramRepository() ..programs.add( Program( metadata: _metadata('program-1'), name: 'Jambes', defaultRestSeconds: 60, tags: const ['match'], exercises: [ ProgramExercise( metadata: _metadata('program-exercise-1'), programId: 'program-1', sourceExerciseId: 'exercise-1', position: 0, exerciseNameSnapshot: 'Tirs', availableTimeSnapshot: false, availableRepsSnapshot: true, availableScoreSnapshot: false, setsCount: 3, timeEnabled: false, repsEnabled: true, scoreEnabled: false, targetReps: 10, ), ], ), ); final exerciseRepository = _FakeExerciseRepository() ..exercises.add( Exercise( metadata: _metadata('exercise-1'), name: 'Tirs', hasTimeMeasure: false, hasRepsMeasure: true, hasScoreMeasure: false, defaultTargetReps: 10, ), ); await tester.pumpWidget( MaterialApp( home: ProgramListScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); await tester.pump(); await tester.pump(); await tester.tap(find.byTooltip('Actions')); await tester.pumpAndSettle(); await tester.tap(find.text('Dupliquer')); await tester.pumpAndSettle(); expect(find.text('Modifier le programme'), findsOneWidget); expect(find.text('Copie de Jambes'), findsOneWidget); expect( find.text("Programme dupliqué. Ajuste la copie avant de l'utiliser."), findsOneWidget, ); expect(find.widgetWithText(Chip, 'match'), findsOneWidget); }); testWidgets('modifier un programme exemple conserve sa provenance', ( tester, ) async { final programRepository = _FakeProgramRepository() ..programs.add( Program( metadata: _metadata('program-1'), name: 'Fondations basket - 45 min', defaultRestSeconds: 45, isExample: true, exercises: [ ProgramExercise( metadata: _metadata('program-exercise-1'), programId: 'program-1', sourceExerciseId: 'exercise-1', position: 0, exerciseNameSnapshot: 'Tirs', availableTimeSnapshot: false, availableRepsSnapshot: true, availableScoreSnapshot: false, setsCount: 3, timeEnabled: false, repsEnabled: true, scoreEnabled: false, targetReps: 10, ), ], ), ); final exerciseRepository = _FakeExerciseRepository() ..exercises.add( Exercise( metadata: _metadata('exercise-1'), name: 'Tirs', hasTimeMeasure: false, hasRepsMeasure: true, hasScoreMeasure: false, defaultTargetReps: 10, ), ); await tester.pumpWidget( MaterialApp( home: ProgramFormScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), program: programRepository.programs.single, ), ), ); await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Edité'); await tester.tap(find.text('Enregistrer')); await tester.pumpAndSettle(); expect(programRepository.programs.single.name, 'Edité'); expect(programRepository.programs.single.isExample, isTrue); }); 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( 'ajouter un exercice avec étapes conserve le snapshot des étapes', (tester) async { final steps = [ _step( id: 'step-1', position: 0, name: 'Course', type: ExerciseStepType.time, ), _step( id: 'step-2', position: 1, name: 'Tirs', type: ExerciseStepType.reps, ), ]; final programRepository = _FakeProgramRepository(); final exerciseRepository = _FakeExerciseRepository() ..exercises.add( Exercise( metadata: _metadata('exercise-steps'), name: 'Circuit tirs', hasTimeMeasure: false, hasRepsMeasure: true, hasScoreMeasure: false, defaultTargetReps: 2, steps: steps, ), ); await tester.pumpWidget( MaterialApp( home: ProgramFormScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); await tester.enterText( find.widgetWithText(TextFormField, 'Nom'), 'Programme étapes', ); await tester.tap(find.text('Ajouter un exercice')); await tester.pumpAndSettle(); await tester.tap(find.text('Circuit tirs')); await tester.pumpAndSettle(); await tester.tap(find.text('Enregistrer')); await tester.pumpAndSettle(); final savedSteps = programRepository .programs .single .exercises .single .exerciseStepsSnapshot; expect(savedSteps, hasLength(2)); expect(savedSteps.map((step) => step.name), ['Course', 'Tirs']); expect(savedSteps.map((step) => step.type), [ ExerciseStepType.time, ExerciseStepType.reps, ]); }, ); testWidgets( 'réenregistrer un programme existant conserve les étapes du snapshot', (tester) async { final steps = [ _step( id: 'step-1', position: 0, name: 'Course', type: ExerciseStepType.time, ), _step( id: 'step-2', position: 1, name: 'Tirs', type: ExerciseStepType.reps, ), ]; final programRepository = _FakeProgramRepository() ..programs.add( Program( metadata: _metadata('program-steps'), name: 'Programme étapes', defaultRestSeconds: 45, exercises: [ ProgramExercise( metadata: _metadata('program-exercise-steps'), programId: 'program-steps', sourceExerciseId: 'exercise-steps', position: 0, exerciseNameSnapshot: 'Circuit tirs', exerciseStepsSnapshot: steps, availableTimeSnapshot: false, availableRepsSnapshot: true, availableScoreSnapshot: false, setsCount: 3, timeEnabled: false, repsEnabled: true, scoreEnabled: false, targetReps: 2, ), ], ), ); final exerciseRepository = _FakeExerciseRepository(); await tester.pumpWidget( MaterialApp( home: ProgramFormScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), program: programRepository.programs.single, ), ), ); await tester.tap(find.text('Enregistrer')); await tester.pumpAndSettle(); final savedSteps = programRepository .programs .single .exercises .single .exerciseStepsSnapshot; expect(savedSteps, hasLength(2)); expect(savedSteps.map((step) => step.name), ['Course', 'Tirs']); expect(savedSteps.map((step) => step.position), [0, 1]); }, ); testWidgets( 'personnaliser l’enchaînement automatique transmet l’override au programme', (tester) async { final steps = [ _step( id: 'step-1', position: 0, name: 'Course', type: ExerciseStepType.time, ), _step( id: 'step-2', position: 1, name: 'Retour', type: ExerciseStepType.time, ), ]; final programRepository = _FakeProgramRepository(); final exerciseRepository = _FakeExerciseRepository() ..exercises.add( Exercise( metadata: _metadata('exercise-steps'), name: 'Navettes', hasTimeMeasure: true, hasRepsMeasure: false, hasScoreMeasure: false, defaultTargetTimeSeconds: 30, steps: steps, ), ); await tester.binding.setSurfaceSize(const Size(400, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget( MaterialApp( home: ProgramFormScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); await tester.enterText( find.widgetWithText(TextFormField, 'Nom'), 'Programme étapes', ); await tester.tap(find.text('Ajouter un exercice')); await tester.pumpAndSettle(); await tester.tap(find.text('Navettes')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip("Personnaliser l'exercice")); await tester.pumpAndSettle(); await tester.tap( find.widgetWithText( SwitchListTile, 'Enchaîner automatiquement les chronos consécutifs', ), ); await tester.pump(); expect( find.textContaining('Personnalisé pour ce programme'), findsOneWidget, ); await tester.tap(find.text('Enregistrer')); await tester.pumpAndSettle(); await tester.tap(find.text('Enregistrer')); await tester.pumpAndSettle(); final savedExercise = programRepository.programs.single.exercises.single; expect(savedExercise.autoStartNextTimedStepSnapshot, isTrue); expect(savedExercise.autoStartNextTimedStepOverride, isFalse); }, ); testWidgets( 'revenir au réglage de l’exercice remet l’override programme à null', (tester) async { final steps = [ _step( id: 'step-1', position: 0, name: 'Course', type: ExerciseStepType.time, ), ]; final programRepository = _FakeProgramRepository(); final exerciseRepository = _FakeExerciseRepository() ..exercises.add( Exercise( metadata: _metadata('exercise-steps'), name: 'Navettes', hasTimeMeasure: true, hasRepsMeasure: false, hasScoreMeasure: false, defaultTargetTimeSeconds: 30, autoStartNextTimedStep: false, steps: steps, ), ); await tester.binding.setSurfaceSize(const Size(400, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget( MaterialApp( home: ProgramFormScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), ), ), ); await tester.enterText( find.widgetWithText(TextFormField, 'Nom'), 'Programme étapes', ); await tester.tap(find.text('Ajouter un exercice')); await tester.pumpAndSettle(); await tester.tap(find.text('Navettes')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip("Personnaliser l'exercice")); await tester.pumpAndSettle(); await tester.tap( find.widgetWithText( SwitchListTile, 'Enchaîner automatiquement les chronos consécutifs', ), ); await tester.pump(); expect( find.textContaining('Personnalisé pour ce programme'), findsOneWidget, ); await tester.tap(find.text("Revenir au réglage de l'exercice")); await tester.pump(); expect(find.textContaining("Réglage de l'exercice"), findsOneWidget); await tester.tap(find.text('Enregistrer')); await tester.pumpAndSettle(); await tester.tap(find.text('Enregistrer')); await tester.pumpAndSettle(); final savedExercise = programRepository.programs.single.exercises.single; expect(savedExercise.autoStartNextTimedStepSnapshot, isFalse); expect(savedExercise.autoStartNextTimedStepOverride, isNull); }, ); 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('Actions')); await tester.pumpAndSettle(); await tester.tap(find.text('Supprimer')); 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('Actions')); await tester.pumpAndSettle(); await tester.tap(find.text('Supprimer')); 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); }, ); testWidgets('partager un programme envoie le partage avec un email valide', ( tester, ) async { final programRepository = _FakeProgramRepository() ..programs.add( Program( metadata: _metadata('program-1'), name: 'Programme tirs', defaultRestSeconds: 45, exercises: [ ProgramExercise( metadata: _metadata('program-exercise-1'), programId: 'program-1', sourceExerciseId: 'exercise-1', position: 0, exerciseNameSnapshot: 'Tirs', availableTimeSnapshot: false, availableRepsSnapshot: true, availableScoreSnapshot: false, setsCount: 3, timeEnabled: false, repsEnabled: true, scoreEnabled: false, targetReps: 10, ), ], ), ); final exerciseRepository = _FakeExerciseRepository(); final remoteShareApi = _FakeRemoteShareApi(); await tester.pumpWidget( MaterialApp( home: ProgramFormScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), shareUseCases: _shareUseCases( programRepository: programRepository, remoteShareApi: remoteShareApi, ), program: programRepository.programs.single, ), ), ); await tester.tap(find.byTooltip('Partager')); await tester.pumpAndSettle(); expect(find.text('Partager le programme'), findsOneWidget); await tester.enterText( find.widgetWithText(TextFormField, 'Emails destinataires'), 'joueur@example.com', ); await tester.tap(find.text('Envoyer le partage')); await tester.pump(); await tester.pump(); expect(remoteShareApi.sendCalls, 1); expect(remoteShareApi.lastResourceType, ShareResourceType.program); expect(remoteShareApi.lastRecipientEmails, ['joueur@example.com']); expect(find.text('Partage envoyé.'), findsOneWidget); }); testWidgets('partager sans compte affiche l’écran compte requis', ( tester, ) async { final programRepository = _FakeProgramRepository() ..programs.add( Program( metadata: _metadata('program-1'), name: 'Programme tirs', defaultRestSeconds: 45, ), ); final exerciseRepository = _FakeExerciseRepository(); await tester.pumpWidget( MaterialApp( home: ProgramFormScreen( programUseCases: _programUseCases( programRepository, exerciseRepository, ), exerciseUseCases: _exerciseUseCases(exerciseRepository), shareUseCases: _shareUseCases( tokenStore: _FakeAuthTokenStore(), programRepository: programRepository, ), program: programRepository.programs.single, ), ), ); await tester.tap(find.byTooltip('Partager')); await tester.pumpAndSettle(); await tester.enterText( find.widgetWithText(TextFormField, 'Emails destinataires'), 'joueur@example.com', ); await tester.tap(find.text('Envoyer le partage')); await tester.pumpAndSettle(); expect(find.text('Compte requis pour partager'), findsWidgets); expect(find.text('Annuler'), findsOneWidget); await tester.tap(find.text('Annuler')); await tester.pumpAndSettle(); expect(find.text('Partager le programme'), findsOneWidget); }); } 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', ); } ShareUseCases _shareUseCases({ _FakeAuthTokenStore? tokenStore, _FakeRemoteShareApi? remoteShareApi, _FakeProgramRepository? programRepository, _FakeWorkoutTemplateRepository? templateRepository, }) { return ShareUseCases( tokenStore: tokenStore ?? _FakeAuthTokenStore('token-1'), remoteShareApi: remoteShareApi ?? _FakeRemoteShareApi(), inboxRepository: _FakeShareInboxRepository(), pendingActionRepository: _FakePendingShareActionRepository(), localChanges: _FakeLocalSyncChangeRepository(), programRepository: programRepository ?? _FakeProgramRepository(), 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', ); } ExerciseStep _step({ required String id, required int position, required String name, required ExerciseStepType type, }) { return ExerciseStep( id: id, position: position, name: name, type: type, defaultTargetValue: type == ExerciseStepType.time ? 30 : 5, ); } 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 {} } final class _FakeAuthTokenStore implements AuthTokenStore { _FakeAuthTokenStore([this.token]); String? token; @override Future clearToken() async { token = null; } @override Future readToken() async => token; @override Future saveToken(String token, DateTime expiresAt) async { this.token = token; } } final class _FakeRemoteShareApi implements RemoteShareApi { var sendCalls = 0; ShareResourceType? lastResourceType; List lastRecipientEmails = const []; @override Future acceptShare(String shareId, String token) async { return RemoteSyncedItem( resourceType: SyncResourceType.program, clientId: 'program-remote', serverId: 'program-server', schemaVersion: 1, clientUpdatedAt: DateTime.utc(2026, 7, 17), serverUpdatedAt: DateTime.utc(2026, 7, 17), deletedAt: null, payload: const {'name': 'Programme partagé'}, ); } @override Future declineShare(String shareId, String token) async {} @override Future> fetchInbox(String token) async => const []; @override Future revokeShare(String shareId, String token) async {} @override Future sendShare({ required ShareResourceType resourceType, required Map payload, required List recipientEmails, required String token, }) async { sendCalls += 1; lastResourceType = resourceType; lastRecipientEmails = recipientEmails; return const RemoteShareSendResult( shareId: 'share-1', recipientUserIds: ['recipient-1'], unresolvedEmails: [], ); } } final class _FakeShareInboxRepository implements ShareInboxRepository { @override Future findByShareId(String shareId) async => null; @override Future> listAll() async => const []; @override Future markStatus( String shareId, ShareInboxStatus status, DateTime respondedAt, ) async {} @override Future upsert(ShareInboxItem item) async {} @override Future upsertAll(List items) async {} } final class _FakePendingShareActionRepository implements PendingShareActionRepository { @override Future add(PendingShareAction action) async {} @override Future> listPending() async => const []; @override Future markFailed(String id, DateTime attemptedAt) async {} @override Future markSucceeded(String id, DateTime attemptedAt) async {} } final class _FakeLocalSyncChangeRepository implements LocalSyncChangeRepository { @override Future applyRemoteItem(RemoteSyncedItem item) async => true; @override Future> listPendingChanges() async => const []; @override Future markChangesSynced( List changeLogIds, DateTime syncedAt, ) async {} }