feat(exercice): modèle domain + Drift pour exercices à étapes (ticket #56)
Étend le modèle Drift (tables.dart, app_database.dart/.g.dart, migration schemaVersion 7→8) et la couche application (entités, use cases, repositories) pour supporter des exercices composés de plusieurs étapes. flutter pub get OK, build_runner OK, flutter analyze propre (mêmes 8 infos préexistantes), 87/87 tests verts, build APK debug validé. Premier ticket du chantier "Exercice à plusieurs étapes" (#54). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -79,6 +79,158 @@ void main() {
|
||||
expect(changes.every((change) => change.entityType == 'Exercise'), isTrue);
|
||||
});
|
||||
|
||||
test('exercise repository round-trips configured steps', () async {
|
||||
final now = DateTime.utc(2026, 7, 17, 12);
|
||||
final exercise = Exercise(
|
||||
metadata: _metadata('exercise-steps-1', now),
|
||||
name: 'Burpee complexe',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 1,
|
||||
steps: [
|
||||
ExerciseStep(
|
||||
id: 'step-1',
|
||||
position: 0,
|
||||
name: 'Planche',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 20,
|
||||
),
|
||||
ExerciseStep(
|
||||
id: 'step-2',
|
||||
position: 1,
|
||||
name: 'Sauts',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 8,
|
||||
hasScore: true,
|
||||
scoreLabel: 'Amplitude',
|
||||
scoreUnit: 'pts',
|
||||
defaultTargetScore: 0,
|
||||
),
|
||||
ExerciseStep(
|
||||
id: 'step-3',
|
||||
position: 2,
|
||||
name: 'Sprint final',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 10,
|
||||
hasScore: true,
|
||||
scoreInputMode: ScoreInputMode.stopwatch,
|
||||
defaultTargetScoreTimeMs: 12000,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
await exerciseRepository.save(exercise);
|
||||
|
||||
final restored = await exerciseRepository.findById(exercise.metadata.id);
|
||||
|
||||
expect(restored, isNotNull);
|
||||
expect(restored!.steps, hasLength(3));
|
||||
expect(restored.steps.map((step) => step.name), [
|
||||
'Planche',
|
||||
'Sauts',
|
||||
'Sprint final',
|
||||
]);
|
||||
expect(restored.steps[1].defaultTargetScore, 0);
|
||||
expect(restored.steps[2].scoreInputMode, ScoreInputMode.stopwatch);
|
||||
expect(restored.steps[2].defaultTargetScoreTimeMs, 12000);
|
||||
});
|
||||
|
||||
test(
|
||||
'program exercise step snapshot is preserved when source exercise changes',
|
||||
() async {
|
||||
final now = DateTime.utc(2026, 7, 17, 12);
|
||||
final source = Exercise(
|
||||
metadata: _metadata('exercise-source-1', now),
|
||||
name: 'Complexe original',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 1,
|
||||
steps: [
|
||||
ExerciseStep(
|
||||
id: 'step-original-1',
|
||||
position: 0,
|
||||
name: 'Phase originale A',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 30,
|
||||
),
|
||||
ExerciseStep(
|
||||
id: 'step-original-2',
|
||||
position: 1,
|
||||
name: 'Phase originale B',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 12,
|
||||
),
|
||||
],
|
||||
);
|
||||
await exerciseRepository.save(source);
|
||||
await programRepository.save(
|
||||
Program(
|
||||
metadata: _metadata('program-steps-1', now),
|
||||
name: 'Programme étapes',
|
||||
defaultRestSeconds: 60,
|
||||
),
|
||||
);
|
||||
final useCase = ProgramUseCases(
|
||||
programRepository: programRepository,
|
||||
exerciseRepository: exerciseRepository,
|
||||
templateRepository: templateRepository,
|
||||
clock: _FakeClock(now),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
|
||||
final snapshot = await useCase.addExercise(
|
||||
programId: 'program-steps-1',
|
||||
exerciseId: source.metadata.id,
|
||||
position: 0,
|
||||
setsCount: 1,
|
||||
enabledMeasures: const {WorkoutMeasure.reps},
|
||||
);
|
||||
await exerciseRepository.save(
|
||||
source.copyWith(
|
||||
metadata: _metadata(
|
||||
source.metadata.id,
|
||||
now.add(const Duration(minutes: 1)),
|
||||
1,
|
||||
),
|
||||
steps: [
|
||||
ExerciseStep(
|
||||
id: 'step-updated-1',
|
||||
position: 0,
|
||||
name: 'Phase modifiée',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 45,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final restoredProgram = await programRepository.findById(
|
||||
'program-steps-1',
|
||||
);
|
||||
final restoredSnapshot = restoredProgram!.exercises.single;
|
||||
final snapshotJson = snapshot.toSnapshotJson();
|
||||
|
||||
expect(restoredSnapshot.exerciseStepsSnapshot, hasLength(2));
|
||||
expect(
|
||||
restoredSnapshot.exerciseStepsSnapshot.first.name,
|
||||
'Phase originale A',
|
||||
);
|
||||
expect(
|
||||
restoredSnapshot.exerciseStepsSnapshot.last.type,
|
||||
ExerciseStepType.reps,
|
||||
);
|
||||
expect(
|
||||
(snapshotJson['exerciseStepsSnapshot'] as List).map(
|
||||
(step) => (step as Map)['name'],
|
||||
),
|
||||
['Phase originale A', 'Phase originale B'],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'removing a program exercise writes a soft delete change log entry',
|
||||
() async {
|
||||
|
||||
Reference in New Issue
Block a user