Réorganise l'écran d'exécution autour d'une carte "Exercice actif" sous le compteur SÉRIE X/Y (nom, médias, temps de série, action "Démarrer l'exercice"). Le timer de série devient un état persistant dédié (plus un DateTime UI volatile), pause-aware, et "Démarrer l'exercice" lance en une action tous les chronos qui doivent démarrer en début de série (série, première étape temps, score chrono si stopwatch). Implémentation restée non commitée depuis sa réalisation ; QA a validé les vérifications statiques exécutables en sandbox (dart analyze, git diff --check). flutter analyze / flutter test restent à relancer dans un environnement avec cache Flutter SDK accessible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
827 lines
24 KiB
Dart
827 lines
24 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.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,
|
||
);
|
||
|
||
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);
|
||
});
|
||
|
||
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;
|
||
}
|