feat(exercice): plan de séance et historique avec résultats d'étapes (ticket #61)

Affiche le plan de séance et l'historique avec les résultats par
étape sur workout_execution_screen.dart et history_screen.dart.
flutter pub get OK, dart format appliqué (3 fichiers reformatés),
analyze propre (mêmes infos préexistantes), 102/102 tests verts,
build APK debug validé. Dernier ticket UI du chantier "Exercice à
plusieurs étapes" (#54) — il ne reste que la QA finale (#62).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 20:19:01 +02:00
parent cdaf45646c
commit 8bf4f89276
4 changed files with 557 additions and 62 deletions

View File

@ -75,6 +75,33 @@ void main() {
expect(find.textContaining('Score 38.7'), findsNothing);
});
testWidgets('le détail affiche les résultats détapes par passage', (
tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: HistoryDetailScreen(
history: _history(id: 'history-1', withStepResults: true),
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
workoutTemplateUseCases: _workoutTemplateUseCases(),
activeUseCases: _activeUseCases(),
closeUseCase: _closeUseCase(),
),
),
);
expect(find.text('Étapes réalisées'), findsOneWidget);
expect(find.text('Passage 1'), findsOneWidget);
expect(
find.text(
'Étape 1 · Dribble gauche · Temps 00:10 · Réussites : 4 paniers',
),
findsOneWidget,
);
expect(find.text('Étape 2 · Dribble droite'), findsOneWidget);
expect(find.text('Passée'), findsOneWidget);
});
testWidgets('la suppression demande une confirmation', (tester) async {
final repository = _FakeWorkoutHistoryRepository();
@ -114,6 +141,7 @@ WorkoutHistory _history({
required String id,
DateTime? startedAt,
bool stopwatchScore = false,
bool withStepResults = false,
}) {
final start = startedAt ?? DateTime.utc(2026, 7, 17, 10);
return WorkoutHistory(
@ -129,6 +157,7 @@ WorkoutHistory _history({
'sessionId': 'session-1',
'resolvedTemplateSnapshotJson': _resolvedSnapshot(
stopwatchScore: stopwatchScore,
withSteps: withStepResults,
),
'results': [
{
@ -150,10 +179,57 @@ WorkoutHistory _history({
},
],
}),
stepResults: withStepResults
? [
WorkoutHistoryStepResult(
metadata: _metadata('step-result-1'),
workoutHistoryId: id,
programSnapshotId: 'program-snapshot-1',
exerciseSnapshotId: 'exercise-snapshot-1',
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
passageIndex: 0,
stepIndex: 0,
stepSnapshotId: 'step-1',
stepNameSnapshot: 'Dribble gauche',
stepTypeSnapshot: ExerciseStepType.time,
targetValueSnapshot: 10,
hasScoreSnapshot: true,
scoreInputModeSnapshot: ScoreInputMode.manual,
scoreLabelSnapshot: 'Réussites',
scoreUnitSnapshot: 'paniers',
targetScoreSnapshot: 3,
status: SetResultStatus.completed,
actualTimeMs: 10000,
actualScore: 4,
),
WorkoutHistoryStepResult(
metadata: _metadata('step-result-2'),
workoutHistoryId: id,
programSnapshotId: 'program-snapshot-1',
exerciseSnapshotId: 'exercise-snapshot-1',
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
passageIndex: 0,
stepIndex: 1,
stepSnapshotId: 'step-2',
stepNameSnapshot: 'Dribble droite',
stepTypeSnapshot: ExerciseStepType.reps,
targetValueSnapshot: 8,
hasScoreSnapshot: false,
status: SetResultStatus.skipped,
),
]
: const [],
);
}
String _resolvedSnapshot({bool stopwatchScore = false}) {
String _resolvedSnapshot({
bool stopwatchScore = false,
bool withSteps = false,
}) {
return jsonEncode({
'name': 'Séance jambes',
'programs': [
@ -179,6 +255,29 @@ String _resolvedSnapshot({bool stopwatchScore = false}) {
'scoreLabelSnapshot': 'Charge',
if (!stopwatchScore) 'scoreUnitSnapshot': 'kg',
'restSecondsOverride': 0,
if (withSteps)
'exerciseStepsSnapshot': [
{
'id': 'step-1',
'position': 0,
'name': 'Dribble gauche',
'type': ExerciseStepType.time.name,
'defaultTargetValue': 10,
'hasScore': true,
'scoreInputMode': ScoreInputMode.manual.name,
'scoreLabel': 'Réussites',
'scoreUnit': 'paniers',
'defaultTargetScore': 3,
},
{
'id': 'step-2',
'position': 1,
'name': 'Dribble droite',
'type': ExerciseStepType.reps.name,
'defaultTargetValue': 8,
'hasScore': false,
},
],
},
],
}),

View File

@ -837,6 +837,72 @@ void main() {
expect(find.textContaining('Temps réalisé 00:38.7'), findsNothing);
});
testWidgets('le plan affiche le résumé de séquence dune série', (
tester,
) async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final repository = _FakeActiveSessionRepository();
final session = ActiveWorkoutSession(
metadata: _metadata('session-1'),
sourceWorkoutTemplateId: 'template-1',
status: ActiveWorkoutStatus.running,
startedAt: DateTime.utc(2026, 7, 17, 12),
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
elapsedActiveMs: 0,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot(
setsCount: 2,
targetReps: 2,
exerciseSteps: [
_step('step-1', 0, 'Gauche', ExerciseStepType.reps, 4),
_step('step-2', 1, 'Droite', ExerciseStepType.reps, 4),
],
),
);
repository.session = session;
repository.stepResults.add(
ActiveExerciseStepResult(
metadata: _metadata('step-result-1'),
activeWorkoutSessionId: 'session-1',
programSnapshotId: 'template-program-1',
exerciseSnapshotId: 'exercise-1',
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
passageIndex: 0,
stepIndex: 0,
stepSnapshotId: 'step-1',
stepNameSnapshot: 'Gauche',
stepTypeSnapshot: ExerciseStepType.reps,
targetValueSnapshot: 4,
hasScoreSnapshot: false,
status: SetResultStatus.completed,
actualReps: 4,
),
);
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(repository, clock),
stepUseCases: _stepUseCases(repository, clock),
stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
closeUseCase: _closeUseCase(repository, clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
await tester.tap(find.byTooltip('Voir le plan'));
await tester.pumpAndSettle();
expect(find.text('Séquence : 1/2 étapes · Passage 1/2'), findsOneWidget);
});
testWidgets(
'modifier une série passée la termine sans changer la position courante',
(tester) async {
@ -1011,9 +1077,7 @@ void main() {
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot(
targetReps: 1,
exerciseSteps: [
_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 8),
],
exerciseSteps: [_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 8)],
),
);
repository.session = session;
@ -1170,9 +1234,7 @@ void main() {
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot(
targetReps: 2,
exerciseSteps: [
_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 4),
],
exerciseSteps: [_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 4)],
),
);
repository.session = session;
@ -1220,9 +1282,7 @@ void main() {
setsCount: 2,
restSeconds: 0,
targetReps: 2,
exerciseSteps: [
_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 4),
],
exerciseSteps: [_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 4)],
),
);
repository.session = session;
@ -1667,9 +1727,7 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
}
@override
Future<void> saveExerciseStepResult(
ActiveExerciseStepResult result,
) async {
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {
final index = stepResults.indexWhere(
(saved) =>
saved.activeWorkoutSessionId == result.activeWorkoutSessionId &&