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:
2026-07-18 10:18:31 +02:00
parent 6ee06fef01
commit 88af0387da
14 changed files with 882 additions and 27 deletions

View File

@ -38,6 +38,7 @@ void main() {
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
defaultTargetReps: 10,
);
final mediaRepository = _FakeMediaAssetRepository()
..asset = MediaAsset(

View File

@ -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