merge(main): étapes d'exercice jamais affichées en séance (ticket #72)

Fusionne feature/bugfix-72-program-steps-snapshot — pub get OK, dart
format appliqué (aucun changement), analyze propre (mêmes infos
préexistantes), 135/135 tests verts (dont 2 nouveaux tests de
non-régression exerçant le vrai flux UI ProgramFormScreen), build APK
debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 08:10:46 +02:00
7 changed files with 181 additions and 18 deletions

View File

@ -1,6 +1,6 @@
---
issueRef: "#71"
version: 3
updatedBy: {"kind":"user"}
updatedAt: 1784490850142
version: 4
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784526657344
---

View File

@ -2,15 +2,15 @@
id: "b7216520-cf5b-4e65-aeed-1839980d1fe6"
number: 71
title: "[Bug] selection de score par défaut à 0"
status: "open"
status: "qa"
priority: "critical"
sprint: null
links: []
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
createdBy: {"kind":"user"}
updatedBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784490730564
updatedAt: 1784490850142
version: 3
updatedAt: 1784526657344
version: 4
---
Je dois pouvoir mettre un score par défaut à 0 lors de la création d'un exercice. Actuellement quand je met un score à 0, l'application me retourne une StateException car il y avisiblement un check qui se fait un le score > 0

View File

@ -1,6 +1,6 @@
---
issueRef: "#74"
version: 3
updatedBy: {"kind":"user"}
updatedAt: 1784493327494
version: 4
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784526657602
---

View File

@ -2,16 +2,16 @@
id: "c18d9e4a-f6b4-4f6e-bfb5-b77c958f5534"
number: 74
title: "[Bug] Serveur non lancable"
status: "open"
status: "qa"
priority: "critical"
sprint: null
links: []
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
createdBy: {"kind":"user"}
updatedBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784493302766
updatedAt: 1784493327494
version: 3
updatedAt: 1784526657602
version: 4
---
J'ai cette erreur quand j'essaie de build le serveur:
docker compose up --build

View File

@ -799,13 +799,13 @@
"issueRef": "#71",
"path": "71",
"title": "[Bug] selection de score par défaut à 0",
"status": "open",
"status": "qa",
"priority": "critical",
"sprint": null,
"assignedAgentIds": [
"57695b92-24d0-4876-837c-76116e70a6ae"
],
"updatedAt": 1784490850142
"updatedAt": 1784526657344
},
{
"issueRef": "#72",
@ -835,13 +835,13 @@
"issueRef": "#74",
"path": "74",
"title": "[Bug] Serveur non lancable",
"status": "open",
"status": "qa",
"priority": "critical",
"sprint": null,
"assignedAgentIds": [
"57695b92-24d0-4876-837c-76116e70a6ae"
],
"updatedAt": 1784493327494
"updatedAt": 1784526657602
}
]
}

View File

@ -915,6 +915,7 @@ final class _ProgramExerciseDraft {
this.exerciseImageMediaIdSnapshot,
this.exerciseImageMediaIdsSnapshot = const [],
this.exerciseVideoMediaIdSnapshot,
this.exerciseStepsSnapshot = const [],
this.exerciseArchivedSnapshot = false,
required this.availableTimeSnapshot,
required this.availableRepsSnapshot,
@ -944,6 +945,7 @@ final class _ProgramExerciseDraft {
exercise.iconMediaId ?? exercise.imageMediaId,
exerciseImageMediaIdsSnapshot: exercise.imageMediaIds,
exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
exerciseStepsSnapshot: exercise.steps,
exerciseArchivedSnapshot: exercise.archivedAt != null,
availableTimeSnapshot: exercise.hasTimeMeasure,
availableRepsSnapshot: exercise.hasRepsMeasure,
@ -975,6 +977,7 @@ final class _ProgramExerciseDraft {
exerciseImageMediaIdSnapshot: exercise.exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot: exercise.exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exercise.exerciseVideoMediaIdSnapshot,
exerciseStepsSnapshot: exercise.exerciseStepsSnapshot,
exerciseArchivedSnapshot: exercise.exerciseArchivedSnapshot,
availableTimeSnapshot: exercise.availableTimeSnapshot,
availableRepsSnapshot: exercise.availableRepsSnapshot,
@ -1004,6 +1007,7 @@ final class _ProgramExerciseDraft {
final String? exerciseImageMediaIdSnapshot;
final List<String> exerciseImageMediaIdsSnapshot;
final String? exerciseVideoMediaIdSnapshot;
final List<ExerciseStep> exerciseStepsSnapshot;
final bool exerciseArchivedSnapshot;
final bool availableTimeSnapshot;
final bool availableRepsSnapshot;
@ -1029,6 +1033,7 @@ final class _ProgramExerciseDraft {
exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
exerciseStepsSnapshot: exerciseStepsSnapshot,
exerciseArchivedSnapshot: exerciseArchivedSnapshot,
availableTimeSnapshot: availableTimeSnapshot,
availableRepsSnapshot: availableRepsSnapshot,
@ -1057,6 +1062,7 @@ final class _ProgramExerciseDraft {
exerciseImageMediaIdSnapshot: exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
exerciseStepsSnapshot: exerciseStepsSnapshot,
exerciseArchivedSnapshot: exerciseArchivedSnapshot,
availableTimeSnapshot: availableTimeSnapshot,
availableRepsSnapshot: availableRepsSnapshot,

View File

@ -164,6 +164,148 @@ void main() {
},
);
testWidgets(
'ajouter un exercice avec étapes conserve le snapshot des étapes',
(tester) async {
final steps = [
_step(
id: 'step-1',
position: 0,
name: 'Course',
type: ExerciseStepType.time,
),
_step(
id: 'step-2',
position: 1,
name: 'Tirs',
type: ExerciseStepType.reps,
),
];
final programRepository = _FakeProgramRepository();
final exerciseRepository = _FakeExerciseRepository()
..exercises.add(
Exercise(
metadata: _metadata('exercise-steps'),
name: 'Circuit tirs',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
defaultTargetReps: 2,
steps: steps,
),
);
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
programRepository,
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
),
),
);
await tester.enterText(
find.widgetWithText(TextFormField, 'Nom'),
'Programme étapes',
);
await tester.tap(find.text('Ajouter un exercice'));
await tester.pumpAndSettle();
await tester.tap(find.text('Circuit tirs'));
await tester.pumpAndSettle();
await tester.tap(find.text('Enregistrer'));
await tester.pumpAndSettle();
final savedSteps = programRepository
.programs
.single
.exercises
.single
.exerciseStepsSnapshot;
expect(savedSteps, hasLength(2));
expect(savedSteps.map((step) => step.name), ['Course', 'Tirs']);
expect(savedSteps.map((step) => step.type), [
ExerciseStepType.time,
ExerciseStepType.reps,
]);
},
);
testWidgets(
'réenregistrer un programme existant conserve les étapes du snapshot',
(tester) async {
final steps = [
_step(
id: 'step-1',
position: 0,
name: 'Course',
type: ExerciseStepType.time,
),
_step(
id: 'step-2',
position: 1,
name: 'Tirs',
type: ExerciseStepType.reps,
),
];
final programRepository = _FakeProgramRepository()
..programs.add(
Program(
metadata: _metadata('program-steps'),
name: 'Programme étapes',
defaultRestSeconds: 45,
exercises: [
ProgramExercise(
metadata: _metadata('program-exercise-steps'),
programId: 'program-steps',
sourceExerciseId: 'exercise-steps',
position: 0,
exerciseNameSnapshot: 'Circuit tirs',
exerciseStepsSnapshot: steps,
availableTimeSnapshot: false,
availableRepsSnapshot: true,
availableScoreSnapshot: false,
setsCount: 3,
timeEnabled: false,
repsEnabled: true,
scoreEnabled: false,
targetReps: 2,
),
],
),
);
final exerciseRepository = _FakeExerciseRepository();
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
programRepository,
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
program: programRepository.programs.single,
),
),
);
await tester.tap(find.text('Enregistrer'));
await tester.pumpAndSettle();
final savedSteps = programRepository
.programs
.single
.exercises
.single
.exerciseStepsSnapshot;
expect(savedSteps, hasLength(2));
expect(savedSteps.map((step) => step.name), ['Course', 'Tirs']);
expect(savedSteps.map((step) => step.position), [0, 1]);
},
);
testWidgets('affiche lobjectif de chrono pour un score en mode chrono', (
tester,
) async {
@ -510,6 +652,21 @@ EntityMetadata _metadata(String id) {
);
}
ExerciseStep _step({
required String id,
required int position,
required String name,
required ExerciseStepType type,
}) {
return ExerciseStep(
id: id,
position: position,
name: name,
type: type,
defaultTargetValue: type == ExerciseStepType.time ? 30 : 5,
);
}
final class _FakeClock implements Clock {
const _FakeClock(this.value);