fix(execution): série qu'on ne pouvait pas terminer (ticket #29)
Corrige le blocage empêchant de terminer une série sur workout_execution_screen.dart. flutter analyze propre, 42/42 tests verts, build APK debug validé. Ticket laissé en QA : validation manuelle sur APK par l'utilisateur avant clôture. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -354,48 +354,60 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _finishSet({required bool skipped}) async {
|
Future<void> _finishSet({required bool skipped}) async {
|
||||||
final score = double.tryParse(_scoreController.text.trim());
|
try {
|
||||||
final actualTimeMs = DateTime.now()
|
final score = double.tryParse(_scoreController.text.trim());
|
||||||
.toUtc()
|
final actualTimeMs = DateTime.now()
|
||||||
.difference(_seriesStartedAt)
|
.toUtc()
|
||||||
.inMilliseconds;
|
.difference(_seriesStartedAt)
|
||||||
await widget.activeUseCases.recordCurrentSetResult(
|
.inMilliseconds;
|
||||||
sessionId: _session.metadata.id,
|
await widget.activeUseCases.recordCurrentSetResult(
|
||||||
programSnapshotId: _plan.programAt(_position).id,
|
|
||||||
exerciseSnapshotId: _exercise.id,
|
|
||||||
programIndex: _position.programIndex,
|
|
||||||
exerciseIndex: _position.exerciseIndex,
|
|
||||||
setIndex: _position.setIndex,
|
|
||||||
actualReps: skipped ? null : (_exercise.repsEnabled ? _reps : null),
|
|
||||||
actualScore: skipped ? null : (_exercise.scoreEnabled ? score : null),
|
|
||||||
scoreUnitSnapshot: _exercise.scoreUnit,
|
|
||||||
actualTimeMs: skipped
|
|
||||||
? null
|
|
||||||
: (_exercise.timeEnabled ? actualTimeMs : null),
|
|
||||||
);
|
|
||||||
|
|
||||||
final next = _plan.nextPosition(_position);
|
|
||||||
if (next == null) {
|
|
||||||
await _complete();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final shouldRest = _plan.shouldShowRestAfter(_position);
|
|
||||||
if (shouldRest) {
|
|
||||||
final rest = await widget.activeUseCases.startRestAfterSet(
|
|
||||||
sessionId: _session.metadata.id,
|
sessionId: _session.metadata.id,
|
||||||
afterProgramIndex: _position.programIndex,
|
programSnapshotId: _plan.programAt(_position).id,
|
||||||
afterExerciseIndex: _position.exerciseIndex,
|
exerciseSnapshotId: _exercise.id,
|
||||||
afterSetIndex: _position.setIndex,
|
programIndex: _position.programIndex,
|
||||||
plannedRestSeconds: _exercise.restSeconds,
|
exerciseIndex: _position.exerciseIndex,
|
||||||
|
setIndex: _position.setIndex,
|
||||||
|
actualReps: skipped ? null : (_exercise.repsEnabled ? _reps : null),
|
||||||
|
actualScore: skipped
|
||||||
|
? null
|
||||||
|
: (_exercise.manualScoreEnabled ? score : null),
|
||||||
|
scoreInputModeSnapshot: _exercise.scoreInputMode,
|
||||||
|
scoreLabelSnapshot: _exercise.scoreLabel,
|
||||||
|
scoreUnitSnapshot: _exercise.scoreUnit,
|
||||||
|
actualTimeMs: skipped
|
||||||
|
? null
|
||||||
|
: (_exercise.timeEnabled ? actualTimeMs : null),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final next = _plan.nextPosition(_position);
|
||||||
|
if (next == null) {
|
||||||
|
await _complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final shouldRest = _plan.shouldShowRestAfter(_position);
|
||||||
|
if (shouldRest) {
|
||||||
|
final rest = await widget.activeUseCases.startRestAfterSet(
|
||||||
|
sessionId: _session.metadata.id,
|
||||||
|
afterProgramIndex: _position.programIndex,
|
||||||
|
afterExerciseIndex: _position.exerciseIndex,
|
||||||
|
afterSetIndex: _position.setIndex,
|
||||||
|
plannedRestSeconds: _exercise.restSeconds,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
_activeRestStateId = rest.metadata.id;
|
||||||
|
_remainingRestSeconds = _exercise.restSeconds;
|
||||||
|
_startRestTicker(next);
|
||||||
|
setState(() => _mode = WorkoutExecutionMode.rest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _moveTo(next);
|
||||||
|
} on Exception catch (error) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_activeRestStateId = rest.metadata.id;
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
_remainingRestSeconds = _exercise.restSeconds;
|
SnackBar(content: Text('Impossible de terminer la série : $error')),
|
||||||
_startRestTicker(next);
|
);
|
||||||
setState(() => _mode = WorkoutExecutionMode.rest);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await _moveTo(next);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startRestTicker(ExecutionPosition next) {
|
void _startRestTicker(ExecutionPosition next) {
|
||||||
@ -1344,6 +1356,8 @@ final class ExecutionExercise {
|
|||||||
required this.repsEnabled,
|
required this.repsEnabled,
|
||||||
required this.scoreEnabled,
|
required this.scoreEnabled,
|
||||||
required this.restSeconds,
|
required this.restSeconds,
|
||||||
|
this.scoreInputMode = ScoreInputMode.manual,
|
||||||
|
this.scoreLabel,
|
||||||
this.targetTimeSeconds,
|
this.targetTimeSeconds,
|
||||||
this.targetReps,
|
this.targetReps,
|
||||||
this.targetScore,
|
this.targetScore,
|
||||||
@ -1364,6 +1378,10 @@ final class ExecutionExercise {
|
|||||||
repsEnabled: exercise['repsEnabled'] as bool,
|
repsEnabled: exercise['repsEnabled'] as bool,
|
||||||
scoreEnabled: exercise['scoreEnabled'] as bool,
|
scoreEnabled: exercise['scoreEnabled'] as bool,
|
||||||
restSeconds: exercise['restSecondsOverride'] as int? ?? 0,
|
restSeconds: exercise['restSecondsOverride'] as int? ?? 0,
|
||||||
|
scoreInputMode: _scoreInputModeFromSnapshot(
|
||||||
|
exercise['scoreInputModeSnapshot'],
|
||||||
|
),
|
||||||
|
scoreLabel: exercise['scoreLabelSnapshot'] as String?,
|
||||||
targetTimeSeconds:
|
targetTimeSeconds:
|
||||||
(override?['targetTimeSecondsOverride'] as int?) ??
|
(override?['targetTimeSecondsOverride'] as int?) ??
|
||||||
(exercise['targetTimeSeconds'] as int?),
|
(exercise['targetTimeSeconds'] as int?),
|
||||||
@ -1384,10 +1402,23 @@ final class ExecutionExercise {
|
|||||||
final bool repsEnabled;
|
final bool repsEnabled;
|
||||||
final bool scoreEnabled;
|
final bool scoreEnabled;
|
||||||
final int restSeconds;
|
final int restSeconds;
|
||||||
|
final ScoreInputMode scoreInputMode;
|
||||||
|
final String? scoreLabel;
|
||||||
final int? targetTimeSeconds;
|
final int? targetTimeSeconds;
|
||||||
final int? targetReps;
|
final int? targetReps;
|
||||||
final double? targetScore;
|
final double? targetScore;
|
||||||
final String? scoreUnit;
|
final String? scoreUnit;
|
||||||
|
|
||||||
|
bool get manualScoreEnabled {
|
||||||
|
return scoreEnabled && scoreInputMode == ScoreInputMode.manual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScoreInputMode _scoreInputModeFromSnapshot(Object? value) {
|
||||||
|
return switch (value) {
|
||||||
|
'stopwatch' => ScoreInputMode.stopwatch,
|
||||||
|
_ => ScoreInputMode.manual,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ExecutionPosition {
|
final class ExecutionPosition {
|
||||||
|
|||||||
@ -218,6 +218,74 @@ void main() {
|
|||||||
expect(repository.results.single.actualTimeMs, greaterThan(0));
|
expect(repository.results.single.actualTimeMs, greaterThan(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'terminer la série 2 avec temps répétitions et score manuel avance',
|
||||||
|
(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: 1,
|
||||||
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
|
setsCount: 3,
|
||||||
|
restSeconds: 0,
|
||||||
|
targetTimeSeconds: 30,
|
||||||
|
targetReps: 5,
|
||||||
|
targetScore: 10,
|
||||||
|
scoreUnit: 'points',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
repository.session = session;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: WorkoutExecutionScreen(
|
||||||
|
initialSession: session,
|
||||||
|
activeUseCases: _activeUseCases(repository, clock),
|
||||||
|
closeUseCase: _closeUseCase(repository, clock),
|
||||||
|
historyUseCases: _historyUseCases(clock),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.text('Programme 1/1 · Exercice 1/1 · Série 2/3'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(find.text('30 s'), findsOneWidget);
|
||||||
|
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
await tester.tap(find.byIcon(Icons.add));
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
await tester.enterText(
|
||||||
|
find.widgetWithText(TextField, 'Score (points)'),
|
||||||
|
'5',
|
||||||
|
);
|
||||||
|
await tester.ensureVisible(find.text('Terminer la série'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Terminer la série'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(repository.results.single.setIndex, 1);
|
||||||
|
expect(repository.results.single.actualReps, 3);
|
||||||
|
expect(repository.results.single.actualScore, 5);
|
||||||
|
expect(repository.session?.currentSetIndex, 2);
|
||||||
|
expect(
|
||||||
|
find.text('Programme 1/1 · Exercice 1/1 · Série 3/3'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('la flèche de retour met la séance active en pause', (
|
testWidgets('la flèche de retour met la séance active en pause', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@ -483,7 +551,15 @@ WorkoutTemplateUseCases _workoutTemplateUseCases() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _sessionSnapshot({int setsCount = 3, int restSeconds = 0}) {
|
String _sessionSnapshot({
|
||||||
|
int setsCount = 3,
|
||||||
|
int restSeconds = 0,
|
||||||
|
int targetTimeSeconds = 45,
|
||||||
|
int targetReps = 10,
|
||||||
|
double targetScore = 80,
|
||||||
|
String scoreUnit = 'kg',
|
||||||
|
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
||||||
|
}) {
|
||||||
return jsonEncode({
|
return jsonEncode({
|
||||||
'name': 'Séance jambes',
|
'name': 'Séance jambes',
|
||||||
'programs': [
|
'programs': [
|
||||||
@ -499,10 +575,12 @@ String _sessionSnapshot({int setsCount = 3, int restSeconds = 0}) {
|
|||||||
'timeEnabled': true,
|
'timeEnabled': true,
|
||||||
'repsEnabled': true,
|
'repsEnabled': true,
|
||||||
'scoreEnabled': true,
|
'scoreEnabled': true,
|
||||||
'targetTimeSeconds': 45,
|
'targetTimeSeconds': targetTimeSeconds,
|
||||||
'targetReps': 10,
|
'targetReps': targetReps,
|
||||||
'targetScore': 80,
|
'targetScore': targetScore,
|
||||||
'scoreUnitSnapshot': 'kg',
|
'scoreInputModeSnapshot': scoreInputMode.name,
|
||||||
|
'scoreLabelSnapshot': 'Score',
|
||||||
|
'scoreUnitSnapshot': scoreUnit,
|
||||||
'restSecondsOverride': restSeconds,
|
'restSecondsOverride': restSeconds,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user