fix(exercice): suppression d'exercice ne casse plus les programmes (ticket #62)

Corrige ExerciseUseCases.delete() qui cascadait à tort vers les
ProgramExercise référents, violant le pattern snapshot (un exercice
supprimé ne doit jamais affecter les programmes déjà composés). Ajoute
le test de régression associé. Met à jour le texte de confirmation
_confirmDelete dans exercise_library_screen.dart, devenu incohérent
avec ce fix (il affirmait à tort que la suppression retirerait
l'exercice des programmes). Trouvé pendant la QA finale du chantier
"Exercice à plusieurs étapes" (#54). flutter analyze propre (mêmes
infos préexistantes), 103/103 tests verts, build APK debug validé.
Dernier ticket du chantier (#56/#58/#59/#60/#61/#62 tous faits).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 20:56:28 +02:00
parent fa3beb05d4
commit b7ba885b07
4 changed files with 181 additions and 115 deletions

View File

@ -479,7 +479,7 @@ void main() {
);
});
test('delete exercise removes it from referencing programs', () async {
test('delete exercise keeps existing program snapshots unchanged', () async {
final exerciseRepository = _FakeExerciseRepository()
..exercise = Exercise(
metadata: _metadata('exercise-1'),
@ -502,12 +502,6 @@ void main() {
sourceExerciseId: 'exercise-1',
position: 0,
),
_programExercise(
id: 'program-exercise-2',
programId: 'program-1',
sourceExerciseId: 'exercise-2',
position: 1,
),
],
),
);
@ -523,7 +517,11 @@ void main() {
expect(programRepository.programs.single.exercises, hasLength(1));
expect(
programRepository.programs.single.exercises.single.sourceExerciseId,
'exercise-2',
'exercise-1',
);
expect(
programRepository.programs.single.exercises.single.exerciseNameSnapshot,
'Exercise program-exercise-1',
);
expect(programRepository.programs.single.exercises.single.position, 0);
});
@ -1020,6 +1018,95 @@ void main() {
expect(stopped.accumulatedMs, 4000);
});
test(
'exercise step progress auto-advances consecutive elapsed timers after kill',
() async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final session = _sessionWithSnapshot(
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
setsCount: 1,
targetReps: 2,
exerciseSteps: [
_step(
id: 'step-1',
position: 0,
name: 'Gauche',
type: ExerciseStepType.time,
defaultTargetValue: 1,
),
_step(
id: 'step-2',
position: 1,
name: 'Droite',
type: ExerciseStepType.time,
defaultTargetValue: 1,
),
],
);
final repository = _FakeActiveSessionRepository()..session = session;
final useCase = ActiveExerciseStepUseCases(
sessionRepository: repository,
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
);
await useCase.startOrResumeProgress(
sessionId: session.metadata.id,
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
);
await useCase.startTimer(
sessionId: session.metadata.id,
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
);
clock.value = clock.value.add(const Duration(milliseconds: 4500));
final reconstructed = ActiveExerciseStepUseCases(
sessionRepository: repository,
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
);
final view = await reconstructed.startOrResumeProgress(
sessionId: session.metadata.id,
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
);
expect(
view.state.status,
ActiveExerciseStepProgressStatus.sequenceComplete,
);
expect(repository.stepResults, hasLength(4));
expect(repository.stepResults.map((result) => result.passageIndex), [
0,
0,
1,
1,
]);
expect(repository.stepResults.map((result) => result.stepIndex), [
0,
1,
0,
1,
]);
expect(repository.stepResults.map((result) => result.actualTimeMs), [
1000,
1000,
1000,
1000,
]);
},
);
test('score result enforces manual xor stopwatch values', () {
expect(
() => ActiveSetResult(
@ -1431,9 +1518,7 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
}
@override
Future<void> saveExerciseStepResult(
ActiveExerciseStepResult result,
) async {
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {
stepResults.removeWhere(
(existing) =>
existing.activeWorkoutSessionId == result.activeWorkoutSessionId &&
@ -1484,6 +1569,8 @@ ActiveWorkoutSession _sessionWithSnapshot({
required int currentExerciseIndex,
required int currentSetIndex,
int setsCount = 2,
int targetReps = 10,
List<ExerciseStep> exerciseSteps = const [],
}) {
return ActiveWorkoutSession(
metadata: _metadata('session-1'),
@ -1508,7 +1595,10 @@ ActiveWorkoutSession _sessionWithSnapshot({
'timeEnabled': false,
'repsEnabled': true,
'scoreEnabled': false,
'targetReps': 10,
'targetReps': targetReps,
'exerciseStepsSnapshot': exerciseSteps
.map((step) => step.toSnapshotJson())
.toList(),
},
],
}),