From 1aa2304328201c855088029e5959e861baabbf3c Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 18 Jul 2026 22:35:52 +0200 Subject: [PATCH] =?UTF-8?q?fix(exercice):=20score=20par=20d=C3=A9faut=20?= =?UTF-8?q?=C3=A0=200=20et=20affichage=20des=20r=C3=A9p=C3=A9titions=20en?= =?UTF-8?q?=20s=C3=A9ance=20(tickets=20#44,=20#45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Autorise un score par défaut à 0 (#44) et corrige l'affichage des répétitions à 0 pendant l'exécution au lieu de la vraie cible (#45), sur exercise_library_screen.dart et workout_execution_screen.dart, avec les ajustements domain/use cases associés. flutter analyze propre (mêmes 8 infos préexistantes), 79/79 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 --- lib/application/use_cases.dart | 4 +- lib/domain/entities.dart | 11 ++--- lib/presentation/exercise_library_screen.dart | 12 +++++- .../workout_execution_screen.dart | 8 +++- test/application/use_cases_test.dart | 25 ++++++++++- .../workout_execution_screen_test.dart | 41 ++++++++++++++++++- 6 files changed, 85 insertions(+), 16 deletions(-) diff --git a/lib/application/use_cases.dart b/lib/application/use_cases.dart index e019459..e3d9fa3 100644 --- a/lib/application/use_cases.dart +++ b/lib/application/use_cases.dart @@ -1646,8 +1646,8 @@ void _validateExerciseDefaultTargets({ } if (hasScoreMeasure && scoreInputMode == ScoreInputMode.manual && - (defaultTargetScore == null || defaultTargetScore <= 0)) { - throw const DomainException('Default target score must be positive.'); + (defaultTargetScore == null || defaultTargetScore < 0)) { + throw const DomainException('Default target score must not be negative.'); } if (defaultTargetScore != null && (!hasScoreMeasure || scoreInputMode != ScoreInputMode.manual)) { diff --git a/lib/domain/entities.dart b/lib/domain/entities.dart index 1b34f88..f23e779 100644 --- a/lib/domain/entities.dart +++ b/lib/domain/entities.dart @@ -167,7 +167,10 @@ final class Exercise { 'Default target time seconds', ); _requireNullablePositive(defaultTargetReps, 'Default target reps'); - _requireNullablePositiveDouble(defaultTargetScore, 'Default target score'); + _requireNullableNonNegativeDouble( + defaultTargetScore, + 'Default target score', + ); _requireNullablePositive( defaultTargetScoreTimeMs, 'Default target score time ms', @@ -1035,12 +1038,6 @@ void _requireNullableNonNegativeDouble(double? value, String label) { } } -void _requireNullablePositiveDouble(double? value, String label) { - if (value != null && value <= 0) { - throw DomainException('$label must be positive.'); - } -} - void _requireScoreTargetShape({ required bool scoreEnabled, required ScoreInputMode scoreInputMode, diff --git a/lib/presentation/exercise_library_screen.dart b/lib/presentation/exercise_library_screen.dart index ce155d9..1c3c972 100644 --- a/lib/presentation/exercise_library_screen.dart +++ b/lib/presentation/exercise_library_screen.dart @@ -635,9 +635,9 @@ final class _ExerciseFormScreenState extends State { _scoreInputMode != ScoreInputMode.manual) { return null; } - return _positiveDoubleValidator( + return _nonNegativeDoubleValidator( value, - 'Saisis un score supérieur à 0.', + 'Saisis un score positif ou nul.', ); }, ), @@ -914,6 +914,14 @@ final class _ExerciseFormScreenState extends State { return null; } + String? _nonNegativeDoubleValidator(String? value, String message) { + final number = double.tryParse(value?.trim() ?? ''); + if (number == null || number < 0) { + return message; + } + return null; + } + String _optionalIntText(int? value) { return value == null ? '' : '$value'; } diff --git a/lib/presentation/workout_execution_screen.dart b/lib/presentation/workout_execution_screen.dart index bcef767..7a164df 100644 --- a/lib/presentation/workout_execution_screen.dart +++ b/lib/presentation/workout_execution_screen.dart @@ -72,6 +72,7 @@ final class _WorkoutExecutionScreenState extends State { _mode = _session.status == ActiveWorkoutStatus.running ? WorkoutExecutionMode.active : WorkoutExecutionMode.paused; + _reps = _initialRepsFor(_exercise); _seriesStartedAt = DateTime.now().toUtc(); _ticker = Timer.periodic(const Duration(seconds: 1), (_) { if (mounted) setState(() {}); @@ -808,7 +809,7 @@ final class _WorkoutExecutionScreenState extends State { Future _moveTo(ExecutionPosition position) async { _scoreController.clear(); - _reps = 0; + _reps = _initialRepsFor(_plan.exerciseAt(position)); _activeRestStateId = null; _scoreStopwatch = null; _manualScoreTimeMs = null; @@ -889,6 +890,11 @@ final class _WorkoutExecutionScreenState extends State { ), ); } + + int _initialRepsFor(ExecutionExercise exercise) { + if (!exercise.repsEnabled) return 0; + return exercise.targetReps ?? 0; + } } final class SetMeasureInput extends StatelessWidget { diff --git a/test/application/use_cases_test.dart b/test/application/use_cases_test.dart index b40aa42..33ebcbc 100644 --- a/test/application/use_cases_test.dart +++ b/test/application/use_cases_test.dart @@ -55,7 +55,7 @@ void main() { }, ); - test('Exercise use case rejects non-positive default targets', () async { + test('Exercise use case rejects invalid default targets', () async { final useCase = _exerciseUseCase(_FakeExerciseRepository()); await expectLater( @@ -86,12 +86,33 @@ void main() { hasScoreMeasure: true, scoreLabel: 'Charge', scoreUnit: 'kg', - defaultTargetScore: 0, + defaultTargetScore: -1, ), throwsA(isA()), ); }); + test( + 'Exercise use case accepts a zero manual score default target', + () async { + final repository = _FakeExerciseRepository(); + final useCase = _exerciseUseCase(repository); + + final exercise = await useCase.create( + name: 'Charge', + hasTimeMeasure: false, + hasRepsMeasure: false, + hasScoreMeasure: true, + scoreLabel: 'Charge', + scoreUnit: 'kg', + defaultTargetScore: 0, + ); + + expect(exercise.defaultTargetScore, 0); + expect(repository.saved.single.defaultTargetScore, 0); + }, + ); + test( 'Exercise use case accepts stopwatch score without default target', () async { diff --git a/test/presentation/workout_execution_screen_test.dart b/test/presentation/workout_execution_screen_test.dart index 352fcc3..0fedc1c 100644 --- a/test/presentation/workout_execution_screen_test.dart +++ b/test/presentation/workout_execution_screen_test.dart @@ -43,6 +43,42 @@ void main() { expect(find.text('00:45'), findsOneWidget); }); + testWidgets('les répétitions sont initialisées avec la cible', ( + tester, + ) async { + final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); + 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(targetReps: 8), + ); + + await tester.pumpWidget( + MaterialApp( + home: WorkoutExecutionScreen( + initialSession: session, + activeUseCases: _activeUseCases( + _FakeActiveSessionRepository(), + clock, + ), + closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock), + historyUseCases: _historyUseCases(clock), + workoutTemplateUseCases: _workoutTemplateUseCases(), + ), + ), + ); + + expect(find.text('Répétitions'), findsOneWidget); + expect(find.text('8'), findsOneWidget); + }); + testWidgets('les mesures cumulées respectent la hiérarchie visuelle', ( tester, ) async { @@ -618,8 +654,8 @@ void main() { expect(find.text('2 / 3'), findsOneWidget); expect(find.text('30 s'), findsOneWidget); - for (var i = 0; i < 3; i++) { - await tester.tap(find.byIcon(Icons.add)); + for (var i = 0; i < 2; i++) { + await tester.tap(find.byIcon(Icons.remove)); await tester.pump(); } await tester.enterText( @@ -637,6 +673,7 @@ void main() { expect(repository.session?.currentSetIndex, 2); expect(find.text('Programme 1/1 · Exercice 1/1'), findsOneWidget); expect(find.text('3 / 3'), findsOneWidget); + expect(find.text('5'), findsOneWidget); }, );