feat(exercice): galerie de 5 images par exercice (ticket #35)
Étend le modèle Drift (tables.dart, app_database.dart/.g.dart), les entités/use cases/repositories et exercise_library_screen.dart pour supporter jusqu'à 5 images par exercice au lieu d'une seule. flutter analyze propre, 63/63 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -156,6 +156,75 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('Exercise image gallery rejects a sixth image', () async {
|
||||
final repository = _FakeExerciseRepository()
|
||||
..exercise = Exercise(
|
||||
metadata: _metadata('exercise-1'),
|
||||
name: 'Squat',
|
||||
imageMediaIds: const [
|
||||
'media-1',
|
||||
'media-2',
|
||||
'media-3',
|
||||
'media-4',
|
||||
'media-5',
|
||||
],
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 10,
|
||||
);
|
||||
final useCase = _exerciseUseCase(repository);
|
||||
|
||||
await expectLater(
|
||||
useCase.addImage(exerciseId: 'exercise-1', mediaAssetId: 'media-6'),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exercise image gallery reorders images', () async {
|
||||
final repository = _FakeExerciseRepository()
|
||||
..exercise = Exercise(
|
||||
metadata: _metadata('exercise-1'),
|
||||
name: 'Squat',
|
||||
imageMediaIds: const ['media-1', 'media-2', 'media-3'],
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 10,
|
||||
);
|
||||
final useCase = _exerciseUseCase(repository);
|
||||
|
||||
final updated = await useCase.reorderImages(
|
||||
exerciseId: 'exercise-1',
|
||||
orderedMediaIds: const ['media-3', 'media-1', 'media-2'],
|
||||
);
|
||||
|
||||
expect(updated.imageMediaIds, ['media-3', 'media-1', 'media-2']);
|
||||
expect(repository.saved.single.imageMediaIds, updated.imageMediaIds);
|
||||
});
|
||||
|
||||
test('Exercise image gallery removes an image', () async {
|
||||
final repository = _FakeExerciseRepository()
|
||||
..exercise = Exercise(
|
||||
metadata: _metadata('exercise-1'),
|
||||
name: 'Squat',
|
||||
imageMediaIds: const ['media-1', 'media-2', 'media-3'],
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 10,
|
||||
);
|
||||
final useCase = _exerciseUseCase(repository);
|
||||
|
||||
final updated = await useCase.removeImage(
|
||||
exerciseId: 'exercise-1',
|
||||
mediaAssetId: 'media-2',
|
||||
);
|
||||
|
||||
expect(updated.imageMediaIds, ['media-1', 'media-3']);
|
||||
expect(updated.imageMediaId, 'media-1');
|
||||
});
|
||||
|
||||
test('delete exercise removes it from referencing programs', () async {
|
||||
final exerciseRepository = _FakeExerciseRepository()
|
||||
..exercise = Exercise(
|
||||
|
||||
@ -132,23 +132,90 @@ void main() {
|
||||
home: ExerciseFormScreen(
|
||||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||
mediaUseCases: _mediaUseCases(exerciseRepository, storage: storage),
|
||||
mediaPicker: const _FakeMediaPicker(
|
||||
imagePath: '/tmp/imports/photo.png',
|
||||
),
|
||||
mediaPicker: _FakeMediaPicker(imagePath: '/tmp/imports/photo.png'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Choisir une image'));
|
||||
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 importée.'), 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(
|
||||
'supprimer un exercice demande confirmation puis le retire de la liste',
|
||||
(tester) async {
|
||||
@ -244,17 +311,31 @@ MediaUseCases _mediaUseCases(
|
||||
}
|
||||
|
||||
final class _FakeMediaPicker implements MediaSourcePicker {
|
||||
const _FakeMediaPicker({this.imagePath});
|
||||
_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 => imagePath,
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user