feat(gestion): suppression en cascade exercice/programme/séance-modèle (ticket #37)

Ajoute la suppression avec gestion des dépendances en cascade sur les
écrans bibliothèque d'exercices, programme et séance-modèle, avec les
use cases et le bootstrap applicatif associés. flutter analyze propre,
58/58 tests verts, build APK debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 10:33:06 +02:00
parent 7434790da2
commit 1a0d01f038
10 changed files with 881 additions and 26 deletions

View File

@ -148,17 +148,87 @@ void main() {
expect(find.text('Image importée.'), 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 lexercice'));
await tester.pumpAndSettle();
expect(find.text('Supprimer Squat ?'), findsOneWidget);
expect(
find.textContaining('Le supprimer le retirera de ces programmes.'),
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 lexercice'));
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);
},
);
}
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,
@ -174,16 +244,15 @@ MediaUseCases _mediaUseCases(
}
final class _FakeMediaPicker implements MediaSourcePicker {
const _FakeMediaPicker({this.imagePath, this.videoPath});
const _FakeMediaPicker({this.imagePath});
final String? imagePath;
final String? videoPath;
@override
Future<String?> pickPath(MediaKind kind) async {
return switch (kind) {
MediaKind.image => imagePath,
MediaKind.video => videoPath,
MediaKind.video => null,
};
}
}
@ -235,7 +304,11 @@ final class _FakeExerciseRepository implements ExerciseRepository {
Future<bool> isReferencedByProgram(String id) async => referenced;
@override
Future<List<Exercise>> listActive() async => exercises;
Future<List<Exercise>> listActive() async {
return exercises
.where((exercise) => exercise.metadata.deletedAt == null)
.toList();
}
@override
Future<void> save(Exercise exercise) async {