feat(exercice): valeurs par défaut d'exercice (tickets #39/#40, parent #30)
Étend le modèle Drift (tables.dart, app_database.dart/.g.dart), les entités/use cases/repositories et les écrans bibliothèque d'exercices et programme pour supporter des valeurs par défaut par exercice (répétitions/temps/poids proposés à la création d'une série). flutter analyze propre, 51/51 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -38,6 +38,7 @@ void main() {
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 10,
|
||||
);
|
||||
final mediaRepository = _FakeMediaAssetRepository()
|
||||
..asset = MediaAsset(
|
||||
|
||||
@ -18,6 +18,144 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'Exercise use case rejects active measures without default targets',
|
||||
() async {
|
||||
final useCase = _exerciseUseCase(_FakeExerciseRepository());
|
||||
|
||||
await expectLater(
|
||||
useCase.create(
|
||||
name: 'Squat',
|
||||
hasTimeMeasure: true,
|
||||
hasRepsMeasure: false,
|
||||
hasScoreMeasure: false,
|
||||
),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
await expectLater(
|
||||
useCase.create(
|
||||
name: 'Pompes',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
await expectLater(
|
||||
useCase.create(
|
||||
name: 'Charge',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: false,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Charge',
|
||||
scoreUnit: 'kg',
|
||||
),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('Exercise use case rejects non-positive default targets', () async {
|
||||
final useCase = _exerciseUseCase(_FakeExerciseRepository());
|
||||
|
||||
await expectLater(
|
||||
useCase.create(
|
||||
name: 'Squat',
|
||||
hasTimeMeasure: true,
|
||||
hasRepsMeasure: false,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetTimeSeconds: 0,
|
||||
),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
await expectLater(
|
||||
useCase.create(
|
||||
name: 'Pompes',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: -1,
|
||||
),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
await expectLater(
|
||||
useCase.create(
|
||||
name: 'Charge',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: false,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Charge',
|
||||
scoreUnit: 'kg',
|
||||
defaultTargetScore: 0,
|
||||
),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'Exercise use case accepts stopwatch score without default target',
|
||||
() async {
|
||||
final repository = _FakeExerciseRepository();
|
||||
final useCase = _exerciseUseCase(repository);
|
||||
|
||||
final exercise = await useCase.create(
|
||||
name: 'Sprint',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: false,
|
||||
hasScoreMeasure: true,
|
||||
scoreInputMode: ScoreInputMode.stopwatch,
|
||||
scoreLabel: 'Temps réalisé',
|
||||
);
|
||||
|
||||
expect(exercise.defaultTargetScoreTimeMs, isNull);
|
||||
expect(repository.saved.single.metadata.id, exercise.metadata.id);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'Exercise use case rejects non-positive stopwatch score default target',
|
||||
() async {
|
||||
final useCase = _exerciseUseCase(_FakeExerciseRepository());
|
||||
|
||||
await expectLater(
|
||||
useCase.create(
|
||||
name: 'Sprint',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: false,
|
||||
hasScoreMeasure: true,
|
||||
scoreInputMode: ScoreInputMode.stopwatch,
|
||||
scoreLabel: 'Temps réalisé',
|
||||
defaultTargetScoreTimeMs: 0,
|
||||
),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('Exercise update validates default targets too', () async {
|
||||
final repository = _FakeExerciseRepository()
|
||||
..exercise = Exercise(
|
||||
metadata: _metadata('exercise-1'),
|
||||
name: 'Squat',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 10,
|
||||
);
|
||||
final useCase = _exerciseUseCase(repository);
|
||||
|
||||
await expectLater(
|
||||
useCase.update(
|
||||
id: 'exercise-1',
|
||||
name: 'Squat',
|
||||
hasTimeMeasure: true,
|
||||
hasRepsMeasure: false,
|
||||
hasScoreMeasure: false,
|
||||
),
|
||||
throwsA(isA<DomainException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('Workout template use case rejects active measure overrides', () async {
|
||||
final useCase = WorkoutTemplateUseCases(
|
||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||
@ -474,6 +612,38 @@ final class _FakeIds implements IdGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
ExerciseUseCases _exerciseUseCase(_FakeExerciseRepository repository) {
|
||||
return ExerciseUseCases(
|
||||
repository: repository,
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
}
|
||||
|
||||
final class _FakeExerciseRepository implements ExerciseRepository {
|
||||
Exercise? exercise;
|
||||
final saved = <Exercise>[];
|
||||
|
||||
@override
|
||||
Future<Exercise?> findById(String id) async {
|
||||
return exercise?.metadata.id == id ? exercise : null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isReferencedByProgram(String id) async => false;
|
||||
|
||||
@override
|
||||
Future<List<Exercise>> listActive() async =>
|
||||
exercise == null ? const [] : [exercise!];
|
||||
|
||||
@override
|
||||
Future<void> save(Exercise exercise) async {
|
||||
this.exercise = exercise;
|
||||
saved.add(exercise);
|
||||
}
|
||||
}
|
||||
|
||||
final class _FakeWorkoutTemplateRepository
|
||||
implements WorkoutTemplateRepository {
|
||||
@override
|
||||
|
||||
@ -36,6 +36,7 @@ void main() {
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 10,
|
||||
);
|
||||
|
||||
await exerciseRepository.save(exercise);
|
||||
|
||||
@ -51,6 +51,9 @@ void main() {
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Charge',
|
||||
scoreUnit: 'kg',
|
||||
defaultTargetTimeSeconds: 45,
|
||||
defaultTargetReps: 10,
|
||||
defaultTargetScore: 80,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
@ -71,6 +74,9 @@ void main() {
|
||||
) async {
|
||||
final exerciseRepository = _FakeExerciseRepository();
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(400, 2400));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ExerciseFormScreen(
|
||||
@ -81,43 +87,28 @@ void main() {
|
||||
);
|
||||
|
||||
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Run');
|
||||
await tester.dragUntilVisible(
|
||||
find.text('Score'),
|
||||
find.byType(ListView),
|
||||
const Offset(0, -200),
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
||||
'30',
|
||||
);
|
||||
await tester.tap(find.widgetWithText(SwitchListTile, 'Score'));
|
||||
await tester.pump();
|
||||
|
||||
await tester.dragUntilVisible(
|
||||
find.text('Unité'),
|
||||
find.byType(ListView),
|
||||
const Offset(0, -200),
|
||||
);
|
||||
expect(find.widgetWithText(TextFormField, 'Unité'), findsOneWidget);
|
||||
|
||||
await tester.dragUntilVisible(
|
||||
find.text('Chrono intégré'),
|
||||
find.byType(ListView),
|
||||
const Offset(0, 200),
|
||||
);
|
||||
await tester.tap(find.text('Chrono intégré'));
|
||||
await tester.pump();
|
||||
|
||||
await tester.dragUntilVisible(
|
||||
find.text('Temps réalisé'),
|
||||
find.byType(ListView),
|
||||
const Offset(0, -200),
|
||||
);
|
||||
expect(find.text('Temps réalisé'), findsOneWidget);
|
||||
expect(find.widgetWithText(TextFormField, 'Unité'), findsNothing);
|
||||
|
||||
await tester.ensureVisible(find.text('Enregistrer'));
|
||||
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(
|
||||
|
||||
@ -17,6 +17,8 @@ void main() {
|
||||
hasTimeMeasure: true,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetTimeSeconds: 45,
|
||||
defaultTargetReps: 10,
|
||||
),
|
||||
);
|
||||
|
||||
@ -55,6 +57,8 @@ void main() {
|
||||
expect(savedExercise.timeEnabled, isTrue);
|
||||
expect(savedExercise.repsEnabled, isTrue);
|
||||
expect(savedExercise.scoreEnabled, isFalse);
|
||||
expect(savedExercise.targetTimeSeconds, 45);
|
||||
expect(savedExercise.targetReps, 10);
|
||||
expect(savedExercise.restSecondsOverride, 60);
|
||||
},
|
||||
);
|
||||
@ -70,6 +74,7 @@ void main() {
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 12,
|
||||
),
|
||||
);
|
||||
|
||||
@ -114,6 +119,7 @@ void main() {
|
||||
hasTimeMeasure: true,
|
||||
hasRepsMeasure: false,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetTimeSeconds: 45,
|
||||
),
|
||||
);
|
||||
|
||||
@ -171,6 +177,9 @@ void main() {
|
||||
hasScoreMeasure: true,
|
||||
scoreInputMode: ScoreInputMode.stopwatch,
|
||||
scoreLabel: 'Temps réalisé',
|
||||
defaultTargetTimeSeconds: 30,
|
||||
defaultTargetReps: 5,
|
||||
defaultTargetScoreTimeMs: 12000,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user