Lot frontend F1+F2 : composant library_tags.dart (filtre/suggestions de tags), intégration dans exercise_library_screen, program_screen et workout_template_screen (tags + action duplication rapide), tests associés. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
934 lines
27 KiB
Dart
934 lines
27 KiB
Dart
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 validation au moins une mesure active bloque la sauvegarde', (
|
||
tester,
|
||
) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
final exerciseUseCases = _exerciseUseCases(exerciseRepository);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: ExerciseFormScreen(
|
||
exerciseUseCases: exerciseUseCases,
|
||
mediaUseCases: _mediaUseCases(exerciseRepository),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.byType(TextFormField).first, 'Gainage');
|
||
await tester.dragUntilVisible(
|
||
find.widgetWithText(SwitchListTile, 'Temps'),
|
||
find.byType(ListView),
|
||
const Offset(0, -200),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.widgetWithText(SwitchListTile, 'Temps'));
|
||
await tester.pump();
|
||
await tester.dragUntilVisible(
|
||
find.text('Enregistrer'),
|
||
find.byType(ListView),
|
||
const Offset(0, -200),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.text('Enregistrer'));
|
||
await tester.pump();
|
||
|
||
expect(
|
||
find.text('Active au moins une mesure pour enregistrer.'),
|
||
findsOneWidget,
|
||
);
|
||
expect(exerciseRepository.saved, isEmpty);
|
||
});
|
||
|
||
testWidgets('la liste affiche les badges des mesures disponibles', (
|
||
tester,
|
||
) async {
|
||
final exercise = Exercise(
|
||
metadata: _metadata('exercise-1'),
|
||
name: 'Squat',
|
||
description: 'Jambes · charge libre',
|
||
hasTimeMeasure: true,
|
||
hasRepsMeasure: true,
|
||
hasScoreMeasure: true,
|
||
scoreLabel: 'Charge',
|
||
scoreUnit: 'kg',
|
||
defaultTargetTimeSeconds: 45,
|
||
defaultTargetReps: 10,
|
||
defaultTargetScore: 80,
|
||
category: ExerciseCategory.finishing,
|
||
isExample: true,
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(body: ExerciseListTile(exercise: exercise)),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Squat'), findsOneWidget);
|
||
expect(find.text('Jambes · charge libre'), findsOneWidget);
|
||
expect(find.text('Temps'), findsOneWidget);
|
||
expect(find.text('Répétitions'), findsOneWidget);
|
||
expect(find.text('Score (kg)'), findsOneWidget);
|
||
expect(find.text('Finition'), findsOneWidget);
|
||
expect(find.text('Exemple'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('ajouter un tag à un exercice le sauvegarde normalisé', (
|
||
tester,
|
||
) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: ExerciseFormScreen(
|
||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||
mediaUseCases: _mediaUseCases(exerciseRepository),
|
||
tagSuggestions: const ['match'],
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Tirs');
|
||
await tester.tap(find.text('Ajouter un tag'));
|
||
await tester.pumpAndSettle();
|
||
await tester.enterText(
|
||
find.widgetWithText(TextField, 'Nom du tag'),
|
||
' Match ',
|
||
);
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Ajouter'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.widgetWithText(Chip, 'match'), findsOneWidget);
|
||
|
||
await tester.dragUntilVisible(
|
||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
||
find.byType(ListView),
|
||
const Offset(0, -200),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
||
'30',
|
||
);
|
||
await tester.dragUntilVisible(
|
||
find.text('Enregistrer'),
|
||
find.byType(ListView),
|
||
const Offset(0, -200),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.text('Enregistrer'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(exerciseRepository.saved.single.tags, ['match']);
|
||
});
|
||
|
||
testWidgets('filtre les exercices par tag et efface tous les filtres', (
|
||
tester,
|
||
) async {
|
||
final exerciseRepository = _FakeExerciseRepository()
|
||
..exercises.addAll([
|
||
Exercise(
|
||
metadata: _metadata('exercise-1'),
|
||
name: 'Tirs match',
|
||
hasTimeMeasure: true,
|
||
hasRepsMeasure: false,
|
||
hasScoreMeasure: false,
|
||
tags: const ['match', 'extérieur'],
|
||
),
|
||
Exercise(
|
||
metadata: _metadata('exercise-2'),
|
||
name: 'Dribble routine',
|
||
hasTimeMeasure: false,
|
||
hasRepsMeasure: true,
|
||
hasScoreMeasure: false,
|
||
tags: const ['routine'],
|
||
),
|
||
]);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: ExerciseLibraryScreen(
|
||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||
mediaUseCases: _mediaUseCases(exerciseRepository),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
await tester.pump();
|
||
|
||
await tester.tap(find.widgetWithText(FilterChip, 'match'));
|
||
await tester.pump();
|
||
|
||
expect(find.text('Tirs match'), findsOneWidget);
|
||
expect(find.text('Dribble routine'), findsNothing);
|
||
expect(find.text('Effacer les filtres'), findsOneWidget);
|
||
|
||
await tester.tap(find.text('Effacer les filtres'));
|
||
await tester.pump();
|
||
|
||
expect(find.text('Tirs match'), findsOneWidget);
|
||
expect(find.text('Dribble routine'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('basculer en chrono intégré masque l’unité et affiche le badge', (
|
||
tester,
|
||
) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
|
||
await tester.binding.setSurfaceSize(const Size(400, 2400));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: ExerciseFormScreen(
|
||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||
mediaUseCases: _mediaUseCases(exerciseRepository),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Run');
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
||
'30',
|
||
);
|
||
await tester.tap(find.widgetWithText(SwitchListTile, 'Score'));
|
||
await tester.pump();
|
||
|
||
expect(find.widgetWithText(TextFormField, 'Unité'), findsOneWidget);
|
||
|
||
await tester.tap(find.text('Chrono intégré'));
|
||
await tester.pump();
|
||
|
||
expect(find.text('Temps réalisé'), findsOneWidget);
|
||
expect(find.widgetWithText(TextFormField, 'Unité'), findsNothing);
|
||
|
||
await tester.tap(find.text('Enregistrer'));
|
||
await tester.pumpAndSettle();
|
||
|
||
final saved = exerciseRepository.saved.single;
|
||
expect(saved.scoreInputMode, ScoreInputMode.stopwatch);
|
||
expect(saved.defaultTargetTimeSeconds, 30);
|
||
expect(saved.defaultTargetScoreTimeMs, isNull);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(body: ExerciseListTile(exercise: saved)),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Score chrono'), findsOneWidget);
|
||
expect(find.text('Score'), findsNothing);
|
||
});
|
||
|
||
testWidgets(
|
||
'sélectionner une image déclenche l’import et confirme le fichier',
|
||
(tester) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
final storage = _FakeMediaStorage();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: ExerciseFormScreen(
|
||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||
mediaUseCases: _mediaUseCases(exerciseRepository, storage: storage),
|
||
mediaPicker: _FakeMediaPicker(imagePath: '/tmp/imports/photo.png'),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.text('Ajouter une image'));
|
||
await tester.pump();
|
||
|
||
expect(storage.imports.single.sourcePath, '/tmp/imports/photo.png');
|
||
expect(storage.imports.single.kind, MediaKind.image);
|
||
expect(find.text('photo.png'), findsOneWidget);
|
||
expect(find.text('Image ajoutée.'), findsOneWidget);
|
||
},
|
||
);
|
||
|
||
testWidgets('ajouter des images bloque la sixième image', (tester) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
final storage = _FakeMediaStorage();
|
||
|
||
await tester.binding.setSurfaceSize(const Size(400, 1200));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: ExerciseFormScreen(
|
||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||
mediaUseCases: _mediaUseCases(exerciseRepository, storage: storage),
|
||
mediaPicker: _FakeMediaPicker(
|
||
imagePaths: const [
|
||
'/tmp/imports/photo-1.png',
|
||
'/tmp/imports/photo-2.png',
|
||
'/tmp/imports/photo-3.png',
|
||
'/tmp/imports/photo-4.png',
|
||
'/tmp/imports/photo-5.png',
|
||
'/tmp/imports/photo-6.png',
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
for (var i = 0; i < 5; i++) {
|
||
await tester.tap(find.text('Ajouter une image'));
|
||
await tester.pump();
|
||
}
|
||
|
||
expect(storage.imports, hasLength(5));
|
||
expect(find.text('photo-5.png'), findsOneWidget);
|
||
expect(find.text('Maximum 5 images par exercice.'), findsOneWidget);
|
||
|
||
await tester.tap(find.text('Ajouter une image'));
|
||
await tester.pump();
|
||
|
||
expect(storage.imports, hasLength(5));
|
||
expect(find.text('photo-6.png'), findsNothing);
|
||
expect(find.text('Maximum 5 images par exercice.'), findsWidgets);
|
||
});
|
||
|
||
testWidgets('supprimer une image la retire de la galerie', (tester) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
final storage = _FakeMediaStorage();
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: ExerciseFormScreen(
|
||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||
mediaUseCases: _mediaUseCases(exerciseRepository, storage: storage),
|
||
mediaPicker: _FakeMediaPicker(imagePath: '/tmp/imports/photo.png'),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.text('Ajouter une image'));
|
||
await tester.pump();
|
||
|
||
expect(find.text('photo.png'), findsOneWidget);
|
||
|
||
await tester.tap(find.byTooltip('Supprimer l’image'));
|
||
await tester.pump();
|
||
|
||
expect(find.text('photo.png'), findsNothing);
|
||
expect(find.text('Aucune image sélectionnée'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('activer les étapes permet d’ajouter une étape temps', (
|
||
tester,
|
||
) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
|
||
await tester.binding.setSurfaceSize(const Size(400, 3200));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
await _pumpExerciseForm(tester, exerciseRepository);
|
||
await tester.tap(
|
||
find.widgetWithText(
|
||
SwitchListTile,
|
||
'Rythmer cet exercice avec des étapes',
|
||
),
|
||
);
|
||
await tester.pump();
|
||
await tester.tap(find.text('Ajouter une étape'));
|
||
await tester.pump();
|
||
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Nom de l’étape').last,
|
||
'Dribble main droite',
|
||
);
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Durée par défaut (s)').last,
|
||
'12',
|
||
);
|
||
await tester.pump();
|
||
|
||
expect(find.text('1. Dribble main droite'), findsOneWidget);
|
||
expect(find.text('Temps · 12 s'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('ajouter une étape répétitions avec score manuel', (
|
||
tester,
|
||
) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
|
||
await tester.binding.setSurfaceSize(const Size(400, 3600));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
await _pumpExerciseForm(tester, exerciseRepository);
|
||
await tester.tap(
|
||
find.widgetWithText(
|
||
SwitchListTile,
|
||
'Rythmer cet exercice avec des étapes',
|
||
),
|
||
);
|
||
await tester.pump();
|
||
await tester.tap(find.text('Ajouter une étape'));
|
||
await tester.pump();
|
||
await tester.tap(_stepTypeRadio('Répétitions').last);
|
||
await tester.pump();
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Nom de l’étape').last,
|
||
'Pompes',
|
||
);
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Répétitions par défaut').last,
|
||
'10',
|
||
);
|
||
await tester.tap(find.widgetWithText(SwitchListTile, 'Score d’étape'));
|
||
await tester.pump();
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Score à saisir').last,
|
||
'Réussites',
|
||
);
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Unité').last,
|
||
'paniers',
|
||
);
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Score par défaut').last,
|
||
'0',
|
||
);
|
||
await tester.pump();
|
||
|
||
expect(find.text('1. Pompes'), findsOneWidget);
|
||
expect(find.text('Répétitions · 10'), findsOneWidget);
|
||
expect(find.text('Mode de score'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('réordonner les étapes via Monter Descendre', (tester) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
|
||
await tester.binding.setSurfaceSize(const Size(400, 4200));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
await _pumpExerciseForm(tester, exerciseRepository);
|
||
await tester.tap(
|
||
find.widgetWithText(
|
||
SwitchListTile,
|
||
'Rythmer cet exercice avec des étapes',
|
||
),
|
||
);
|
||
await tester.pump();
|
||
await tester.tap(find.text('Ajouter une étape'));
|
||
await tester.pump();
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Nom de l’étape').at(0),
|
||
'Première',
|
||
);
|
||
await tester.tap(find.text('Ajouter une étape'));
|
||
await tester.pump();
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Nom de l’étape').at(1),
|
||
'Deuxième',
|
||
);
|
||
await tester.pump();
|
||
|
||
await tester.tap(find.byTooltip('Actions de l’étape 2'));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.text('Monter'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('1. Deuxième'), findsOneWidget);
|
||
expect(find.text('2. Première'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('supprimer une étape demande confirmation', (tester) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
|
||
await tester.binding.setSurfaceSize(const Size(400, 3200));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
await _pumpExerciseForm(tester, exerciseRepository);
|
||
await tester.tap(
|
||
find.widgetWithText(
|
||
SwitchListTile,
|
||
'Rythmer cet exercice avec des étapes',
|
||
),
|
||
);
|
||
await tester.pump();
|
||
await tester.tap(find.text('Ajouter une étape'));
|
||
await tester.pump();
|
||
|
||
await tester.tap(find.byTooltip('Actions de l’étape 1'));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.text('Supprimer'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Supprimer cette étape ?'), findsOneWidget);
|
||
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Supprimer'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Nouvelle étape'), findsNothing);
|
||
});
|
||
|
||
testWidgets('la limite de 8 étapes bloque l’ajout d’une neuvième', (
|
||
tester,
|
||
) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
|
||
await tester.binding.setSurfaceSize(const Size(400, 8000));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
await _pumpExerciseForm(tester, exerciseRepository);
|
||
await tester.tap(
|
||
find.widgetWithText(
|
||
SwitchListTile,
|
||
'Rythmer cet exercice avec des étapes',
|
||
),
|
||
);
|
||
await tester.pump();
|
||
for (var index = 0; index < 8; index++) {
|
||
await tester.tap(find.text('Ajouter une étape'));
|
||
await tester.pump();
|
||
}
|
||
|
||
expect(find.text('Limite atteinte'), findsOneWidget);
|
||
expect(
|
||
find.text('Un exercice peut contenir jusqu’à 8 étapes.'),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.text('Ajouter une étape'), findsNothing);
|
||
});
|
||
|
||
testWidgets('sauvegarder transmet les étapes au use case', (tester) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
|
||
await tester.binding.setSurfaceSize(const Size(400, 3600));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
await _pumpExerciseForm(tester, exerciseRepository);
|
||
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Combo');
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
||
'30',
|
||
);
|
||
await tester.tap(
|
||
find.widgetWithText(
|
||
SwitchListTile,
|
||
'Rythmer cet exercice avec des étapes',
|
||
),
|
||
);
|
||
await tester.pump();
|
||
await tester.tap(find.text('Ajouter une étape'));
|
||
await tester.pump();
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Nom de l’étape').last,
|
||
'Dribble main droite',
|
||
);
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Durée par défaut (s)').last,
|
||
'12',
|
||
);
|
||
|
||
await tester.ensureVisible(find.text('Enregistrer'));
|
||
await tester.tap(find.text('Enregistrer'));
|
||
await tester.pumpAndSettle();
|
||
|
||
final saved = exerciseRepository.saved.single;
|
||
expect(saved.steps, hasLength(1));
|
||
expect(saved.steps.single.id, isNot(startsWith('step-draft-')));
|
||
expect(saved.steps.single.name, 'Dribble main droite');
|
||
expect(saved.steps.single.type, ExerciseStepType.time);
|
||
expect(saved.steps.single.defaultTargetValue, 12);
|
||
});
|
||
|
||
testWidgets(
|
||
'le réglage d’enchaînement automatique des étapes est sauvegardé',
|
||
(tester) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
|
||
await tester.binding.setSurfaceSize(const Size(400, 3600));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
await _pumpExerciseForm(tester, exerciseRepository);
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Nom'),
|
||
'Combo',
|
||
);
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
||
'30',
|
||
);
|
||
await tester.tap(
|
||
find.widgetWithText(
|
||
SwitchListTile,
|
||
'Rythmer cet exercice avec des étapes',
|
||
),
|
||
);
|
||
await tester.pump();
|
||
await tester.tap(
|
||
find.widgetWithText(
|
||
SwitchListTile,
|
||
'Enchaîner automatiquement les chronos consécutifs',
|
||
),
|
||
);
|
||
await tester.pump();
|
||
await tester.tap(find.text('Ajouter une étape'));
|
||
await tester.pump();
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Nom de l’étape').last,
|
||
'Dribble main droite',
|
||
);
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Durée par défaut (s)').last,
|
||
'12',
|
||
);
|
||
|
||
await tester.ensureVisible(find.text('Enregistrer'));
|
||
await tester.tap(find.text('Enregistrer'));
|
||
await tester.pumpAndSettle();
|
||
|
||
final saved = exerciseRepository.saved.single;
|
||
expect(saved.steps, hasLength(1));
|
||
expect(saved.autoStartNextTimedStep, isFalse);
|
||
},
|
||
);
|
||
|
||
testWidgets('désigner une image comme icône l’indique visuellement', (
|
||
tester,
|
||
) async {
|
||
final exerciseRepository = _FakeExerciseRepository();
|
||
final storage = _FakeMediaStorage();
|
||
|
||
await tester.binding.setSurfaceSize(const Size(400, 1600));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: ExerciseFormScreen(
|
||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||
mediaUseCases: _mediaUseCases(exerciseRepository, storage: storage),
|
||
mediaPicker: _FakeMediaPicker(
|
||
imagePaths: const [
|
||
'/tmp/imports/photo-1.png',
|
||
'/tmp/imports/photo-2.png',
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Squat');
|
||
await tester.enterText(
|
||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
||
'45',
|
||
);
|
||
await tester.tap(find.text('Ajouter une image'));
|
||
await tester.pump();
|
||
await tester.tap(find.text('Ajouter une image'));
|
||
await tester.pump();
|
||
|
||
await tester.tap(find.byTooltip('Définir comme icône').last);
|
||
await tester.pump();
|
||
|
||
expect(find.byTooltip('Icône choisie'), findsOneWidget);
|
||
|
||
await tester.tap(find.text('Enregistrer'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(exerciseRepository.saved.single.iconMediaId, 'id-2');
|
||
});
|
||
|
||
testWidgets('ExerciseListTile utilise l’icône choisie', (tester) async {
|
||
final exercise = Exercise(
|
||
metadata: _metadata('exercise-1'),
|
||
name: 'Squat',
|
||
imageMediaIds: const ['image-1', 'image-2'],
|
||
iconMediaId: 'image-2',
|
||
hasTimeMeasure: true,
|
||
hasRepsMeasure: false,
|
||
hasScoreMeasure: false,
|
||
defaultTargetTimeSeconds: 45,
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(body: ExerciseListTile(exercise: exercise)),
|
||
),
|
||
);
|
||
|
||
expect(find.byTooltip('Icône d’exercice image-2'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets(
|
||
'supprimer un exercice demande confirmation puis le retire de la liste',
|
||
(tester) async {
|
||
final exerciseRepository = _FakeExerciseRepository()
|
||
..referenced = true
|
||
..exercises.add(
|
||
Exercise(
|
||
metadata: _metadata('exercise-1'),
|
||
name: 'Squat',
|
||
hasTimeMeasure: true,
|
||
hasRepsMeasure: false,
|
||
hasScoreMeasure: false,
|
||
defaultTargetTimeSeconds: 45,
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: ExerciseLibraryScreen(
|
||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||
mediaUseCases: _mediaUseCases(exerciseRepository),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
await tester.pump();
|
||
|
||
await tester.tap(find.byTooltip('Supprimer l’exercice'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Supprimer Squat ?'), findsOneWidget);
|
||
expect(
|
||
find.textContaining('Les programmes existants restent inchangés.'),
|
||
findsOneWidget,
|
||
);
|
||
|
||
await tester.tap(find.text('Annuler'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Squat'), findsOneWidget);
|
||
expect(exerciseRepository.saved, isEmpty);
|
||
|
||
await tester.tap(find.byTooltip('Supprimer l’exercice'));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Supprimer'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Squat'), findsNothing);
|
||
expect(exerciseRepository.saved.single.metadata.deletedAt, isNotNull);
|
||
},
|
||
);
|
||
}
|
||
|
||
Future<void> _pumpExerciseForm(
|
||
WidgetTester tester,
|
||
_FakeExerciseRepository exerciseRepository,
|
||
) {
|
||
return tester.pumpWidget(
|
||
MaterialApp(
|
||
home: ExerciseFormScreen(
|
||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||
mediaUseCases: _mediaUseCases(exerciseRepository),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Finder _stepTypeRadio(String title) {
|
||
return find.byWidgetPredicate(
|
||
(widget) =>
|
||
widget is RadioListTile<ExerciseStepType> &&
|
||
widget.title is Text &&
|
||
(widget.title as Text).data == title,
|
||
description: 'RadioListTile<ExerciseStepType> "$title"',
|
||
);
|
||
}
|
||
|
||
ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) {
|
||
return ExerciseUseCases(
|
||
repository: repository,
|
||
programRepository: _FakeProgramRepository(),
|
||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||
ids: _FakeIds(),
|
||
originDeviceId: 'device-1',
|
||
);
|
||
}
|
||
|
||
final class _FakeProgramRepository implements ProgramRepository {
|
||
@override
|
||
Future<Program?> findById(String id) async => null;
|
||
|
||
@override
|
||
Future<List<Program>> listActive() async => const [];
|
||
|
||
@override
|
||
Future<void> replaceExercises(Program program, DateTime deletedAt) async {}
|
||
|
||
@override
|
||
Future<void> save(Program program) async {}
|
||
|
||
@override
|
||
Future<void> saveExercise(ProgramExercise exercise) async {}
|
||
}
|
||
|
||
MediaUseCases _mediaUseCases(
|
||
_FakeExerciseRepository exerciseRepository, {
|
||
_FakeMediaStorage? storage,
|
||
}) {
|
||
return MediaUseCases(
|
||
mediaRepository: _FakeMediaAssetRepository(),
|
||
exerciseRepository: exerciseRepository,
|
||
storage: storage ?? _FakeMediaStorage(),
|
||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||
ids: _FakeIds(),
|
||
originDeviceId: 'device-1',
|
||
);
|
||
}
|
||
|
||
final class _FakeMediaPicker implements MediaSourcePicker {
|
||
_FakeMediaPicker({this.imagePath, this.imagePaths = const []});
|
||
|
||
final String? imagePath;
|
||
final List<String> imagePaths;
|
||
var _nextImagePath = 0;
|
||
|
||
@override
|
||
Future<String?> pickPath(MediaKind kind) async {
|
||
return switch (kind) {
|
||
MediaKind.image => _pickImagePath(),
|
||
MediaKind.video => null,
|
||
};
|
||
}
|
||
|
||
String? _pickImagePath() {
|
||
if (imagePaths.isEmpty) {
|
||
return imagePath;
|
||
}
|
||
if (_nextImagePath >= imagePaths.length) {
|
||
return null;
|
||
}
|
||
final path = imagePaths[_nextImagePath];
|
||
_nextImagePath += 1;
|
||
return path;
|
||
}
|
||
}
|
||
|
||
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 = <Exercise>[];
|
||
final saved = <Exercise>[];
|
||
var referenced = false;
|
||
|
||
@override
|
||
Future<Exercise?> findById(String id) async {
|
||
for (final exercise in exercises) {
|
||
if (exercise.metadata.id == id) {
|
||
return exercise;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
@override
|
||
Future<bool> isReferencedByProgram(String id) async => referenced;
|
||
|
||
@override
|
||
Future<List<Exercise>> listActive() async {
|
||
return exercises
|
||
.where((exercise) => exercise.metadata.deletedAt == null)
|
||
.toList();
|
||
}
|
||
|
||
@override
|
||
Future<void> save(Exercise exercise) async {
|
||
saved.add(exercise);
|
||
exercises.removeWhere((item) => item.metadata.id == exercise.metadata.id);
|
||
exercises.add(exercise);
|
||
}
|
||
}
|
||
|
||
final class _FakeMediaAssetRepository implements MediaAssetRepository {
|
||
final saved = <MediaAsset>[];
|
||
|
||
@override
|
||
Future<MediaAsset?> findById(String id) async {
|
||
for (final asset in saved) {
|
||
if (asset.metadata.id == id) {
|
||
return asset;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
@override
|
||
Future<List<MediaAsset>> listActive() async => saved;
|
||
|
||
@override
|
||
Future<void> save(MediaAsset mediaAsset) async {
|
||
saved.add(mediaAsset);
|
||
}
|
||
}
|
||
|
||
final class _FakeMediaStorage implements LocalMediaStorage {
|
||
final imports = <_MediaImportCall>[];
|
||
|
||
@override
|
||
Future<void> deleteByLocalUri(String localUri) async {}
|
||
|
||
@override
|
||
Future<StoredMediaFile> importFile({
|
||
required String sourcePath,
|
||
required MediaKind kind,
|
||
required String stableFileName,
|
||
}) async {
|
||
imports.add(
|
||
_MediaImportCall(
|
||
sourcePath: sourcePath,
|
||
kind: kind,
|
||
stableFileName: stableFileName,
|
||
),
|
||
);
|
||
return StoredMediaFile(
|
||
localUri: 'file:///tmp/$stableFileName',
|
||
mimeType: kind == MediaKind.image ? 'image/png' : 'video/mp4',
|
||
sizeBytes: 1,
|
||
);
|
||
}
|
||
|
||
@override
|
||
Future<Set<String>> listManagedLocalUris() async => const {};
|
||
}
|
||
|
||
final class _MediaImportCall {
|
||
const _MediaImportCall({
|
||
required this.sourcePath,
|
||
required this.kind,
|
||
required this.stableFileName,
|
||
});
|
||
|
||
final String sourcePath;
|
||
final MediaKind kind;
|
||
final String stableFileName;
|
||
}
|