feat(exercice): modèle et migration pour l'auto-enchaînement des chronos d'étapes (ticket #75)
Étend le modèle Drift (migration schemaVersion 13→14, ADD COLUMN) et la couche application (entités, use cases, repositories) pour supporter l'auto-enchaînement des chronos d'étapes. flutter pub get OK, build_runner OK, analyze propre (mêmes infos préexistantes), 141/141 tests verts, build APK debug validé. Premier ticket du chantier "auto-enchaînement des chronos d'étapes" (#73). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -479,6 +479,129 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('program exercise snapshots step chaining setting from exercise', () async {
|
||||
final exerciseRepository = _FakeExerciseRepository()
|
||||
..exercise = Exercise(
|
||||
metadata: _metadata('exercise-1'),
|
||||
name: 'Circuit',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 10,
|
||||
autoStartNextTimedStep: false,
|
||||
);
|
||||
final programRepository = _FakeProgramRepository()
|
||||
..programs.add(
|
||||
Program(
|
||||
metadata: _metadata('program-1'),
|
||||
name: 'Programme',
|
||||
defaultRestSeconds: 60,
|
||||
),
|
||||
);
|
||||
final useCase = ProgramUseCases(
|
||||
programRepository: programRepository,
|
||||
exerciseRepository: exerciseRepository,
|
||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
|
||||
final snapshot = await useCase.addExercise(
|
||||
programId: 'program-1',
|
||||
exerciseId: 'exercise-1',
|
||||
position: 0,
|
||||
setsCount: 3,
|
||||
enabledMeasures: const {WorkoutMeasure.reps},
|
||||
);
|
||||
|
||||
expect(snapshot.autoStartNextTimedStepSnapshot, isFalse);
|
||||
expect(snapshot.autoStartNextTimedStepOverride, isNull);
|
||||
expect(
|
||||
snapshot.toSnapshotJson()['autoStartNextTimedStepSnapshot'],
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('program saveConfigured preserves step chaining override', () async {
|
||||
final programRepository = _FakeProgramRepository();
|
||||
final useCase = ProgramUseCases(
|
||||
programRepository: programRepository,
|
||||
exerciseRepository: _FakeExerciseRepository(),
|
||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
|
||||
final program = await useCase.saveConfigured(
|
||||
name: 'Programme',
|
||||
defaultRestSeconds: 60,
|
||||
exercises: const [
|
||||
ProgramExerciseConfig(
|
||||
exerciseNameSnapshot: 'Circuit',
|
||||
availableTimeSnapshot: false,
|
||||
availableRepsSnapshot: true,
|
||||
availableScoreSnapshot: false,
|
||||
setsCount: 3,
|
||||
enabledMeasures: {WorkoutMeasure.reps},
|
||||
targetReps: 10,
|
||||
autoStartNextTimedStepSnapshot: true,
|
||||
autoStartNextTimedStepOverride: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
expect(program.exercises.single.autoStartNextTimedStepSnapshot, isTrue);
|
||||
expect(program.exercises.single.autoStartNextTimedStepOverride, isFalse);
|
||||
});
|
||||
|
||||
test('workout template override preserves step chaining override', () async {
|
||||
final templateRepository = _FakeWorkoutTemplateRepository();
|
||||
final useCase = WorkoutTemplateUseCases(
|
||||
templateRepository: templateRepository,
|
||||
programRepository: _FakeProgramRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
|
||||
final template = await useCase.saveConfigured(
|
||||
name: 'Séance',
|
||||
programs: [
|
||||
WorkoutTemplateProgramConfig(
|
||||
clientKey: 'program-1',
|
||||
programNameSnapshot: 'Programme',
|
||||
defaultRestSecondsSnapshot: 60,
|
||||
programSnapshotJson: jsonEncode({
|
||||
'exercises': [
|
||||
{
|
||||
'id': 'exercise-snapshot-1',
|
||||
'exerciseNameSnapshot': 'Circuit',
|
||||
'setsCount': 3,
|
||||
'timeEnabled': false,
|
||||
'repsEnabled': true,
|
||||
'scoreEnabled': false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
],
|
||||
overrides: const [
|
||||
WorkoutTemplateExerciseOverrideConfig(
|
||||
workoutTemplateProgramClientKey: 'program-1',
|
||||
snapshotProgramExerciseId: 'exercise-snapshot-1',
|
||||
autoStartNextTimedStepOverride: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
expect(
|
||||
template.overrides.single.autoStartNextTimedStepOverride,
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('delete exercise keeps existing program snapshots unchanged', () async {
|
||||
final exerciseRepository = _FakeExerciseRepository()
|
||||
..exercise = Exercise(
|
||||
|
||||
@ -98,6 +98,100 @@ void main() {
|
||||
expect(restored!.defaultTargetScore, 0);
|
||||
});
|
||||
|
||||
test('step chaining settings round-trip through drift repositories', () async {
|
||||
final now = DateTime.utc(2026, 7, 17, 12);
|
||||
final exercise = Exercise(
|
||||
metadata: _metadata('exercise-chain-1', now),
|
||||
name: 'Circuit',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 10,
|
||||
autoStartNextTimedStep: false,
|
||||
);
|
||||
await exerciseRepository.save(exercise);
|
||||
|
||||
final restoredExercise = await exerciseRepository.findById(
|
||||
exercise.metadata.id,
|
||||
);
|
||||
expect(restoredExercise!.autoStartNextTimedStep, isFalse);
|
||||
|
||||
final programExercise = _programExercise(
|
||||
'program-exercise-chain-1',
|
||||
now,
|
||||
programId: 'program-chain-1',
|
||||
position: 0,
|
||||
autoStartNextTimedStepSnapshot: false,
|
||||
autoStartNextTimedStepOverride: true,
|
||||
);
|
||||
await programRepository.save(
|
||||
Program(
|
||||
metadata: _metadata('program-chain-1', now),
|
||||
name: 'Programme',
|
||||
defaultRestSeconds: 60,
|
||||
exercises: [programExercise],
|
||||
),
|
||||
);
|
||||
|
||||
final restoredProgram = await programRepository.findById('program-chain-1');
|
||||
expect(
|
||||
restoredProgram!.exercises.single.autoStartNextTimedStepSnapshot,
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
restoredProgram.exercises.single.autoStartNextTimedStepOverride,
|
||||
isTrue,
|
||||
);
|
||||
|
||||
final templateProgram = WorkoutTemplateProgram(
|
||||
metadata: _metadata('template-program-chain-1', now),
|
||||
workoutTemplateId: 'template-chain-1',
|
||||
sourceProgramId: 'program-chain-1',
|
||||
position: 0,
|
||||
programNameSnapshot: 'Programme',
|
||||
defaultRestSecondsSnapshot: 60,
|
||||
programSnapshotJson: jsonEncode({
|
||||
'exercises': [programExercise.toSnapshotJson()],
|
||||
}),
|
||||
);
|
||||
await templateRepository.save(
|
||||
WorkoutTemplate(
|
||||
metadata: _metadata('template-chain-1', now),
|
||||
name: 'Séance',
|
||||
programs: [templateProgram],
|
||||
overrides: [
|
||||
WorkoutTemplateExerciseOverride(
|
||||
metadata: _metadata('override-chain-1', now),
|
||||
workoutTemplateProgramId: templateProgram.metadata.id,
|
||||
snapshotProgramExerciseId: programExercise.metadata.id,
|
||||
),
|
||||
WorkoutTemplateExerciseOverride(
|
||||
metadata: _metadata('override-chain-2', now),
|
||||
workoutTemplateProgramId: templateProgram.metadata.id,
|
||||
snapshotProgramExerciseId: 'program-exercise-chain-2',
|
||||
autoStartNextTimedStepOverride: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final restoredTemplate = await templateRepository.findById(
|
||||
'template-chain-1',
|
||||
);
|
||||
expect(
|
||||
restoredTemplate!.overrides
|
||||
.singleWhere((override) => override.metadata.id == 'override-chain-1')
|
||||
.autoStartNextTimedStepOverride,
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
restoredTemplate.overrides
|
||||
.singleWhere((override) => override.metadata.id == 'override-chain-2')
|
||||
.autoStartNextTimedStepOverride,
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('exercise repository round-trips configured steps', () async {
|
||||
final now = DateTime.utc(2026, 7, 17, 12);
|
||||
final exercise = Exercise(
|
||||
@ -453,13 +547,18 @@ EntityMetadata _metadata(String id, DateTime now, [int localRevision = 0]) {
|
||||
ProgramExercise _programExercise(
|
||||
String id,
|
||||
DateTime now, {
|
||||
String programId = 'program-1',
|
||||
required int position,
|
||||
bool autoStartNextTimedStepSnapshot = true,
|
||||
bool? autoStartNextTimedStepOverride,
|
||||
}) {
|
||||
return ProgramExercise(
|
||||
metadata: _metadata(id, now),
|
||||
programId: 'program-1',
|
||||
programId: programId,
|
||||
position: position,
|
||||
exerciseNameSnapshot: 'Exercise $position',
|
||||
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
|
||||
autoStartNextTimedStepOverride: autoStartNextTimedStepOverride,
|
||||
availableTimeSnapshot: false,
|
||||
availableRepsSnapshot: true,
|
||||
availableScoreSnapshot: false,
|
||||
|
||||
Reference in New Issue
Block a user