feat(ui): écran Bibliothèque d'exercices
Ajoute l'écran de bibliothèque d'exercices (presentation/exercise_library_screen.dart) et ajuste la couche application/domaine (bootstrap, ports, use cases, entités, repositories Drift) pour l'alimenter. flutter analyze propre, 9/9 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -180,6 +180,9 @@ final class _FakeExerciseRepository implements ExerciseRepository {
|
||||
return exercise == null ? const [] : [exercise!];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isReferencedByProgram(String id) async => false;
|
||||
|
||||
@override
|
||||
Future<void> save(Exercise exercise) async {
|
||||
saved.add(exercise);
|
||||
|
||||
188
test/presentation/exercise_library_screen_test.dart
Normal file
188
test/presentation/exercise_library_screen_test.dart
Normal file
@ -0,0 +1,188 @@
|
||||
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',
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) {
|
||||
return ExerciseUseCases(
|
||||
repository: repository,
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
}
|
||||
|
||||
MediaUseCases _mediaUseCases(_FakeExerciseRepository exerciseRepository) {
|
||||
return MediaUseCases(
|
||||
mediaRepository: _FakeMediaAssetRepository(),
|
||||
exerciseRepository: exerciseRepository,
|
||||
storage: _FakeMediaStorage(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
}
|
||||
|
||||
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 => exercises;
|
||||
|
||||
@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 {
|
||||
@override
|
||||
Future<void> deleteByLocalUri(String localUri) async {}
|
||||
|
||||
@override
|
||||
Future<StoredMediaFile> importFile({
|
||||
required String sourcePath,
|
||||
required MediaKind kind,
|
||||
required String stableFileName,
|
||||
}) async {
|
||||
return StoredMediaFile(
|
||||
localUri: 'file:///tmp/$stableFileName',
|
||||
mimeType: kind == MediaKind.image ? 'image/png' : 'video/mp4',
|
||||
sizeBytes: 1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Set<String>> listManagedLocalUris() async => const {};
|
||||
}
|
||||
Reference in New Issue
Block a user