From e3c7ca0dabcffbfa2613e1806a84a2b2d4ff1f53 Mon Sep 17 00:00:00 2001 From: Blomios Date: Tue, 28 Jul 2026 09:37:44 +0200 Subject: [PATCH] =?UTF-8?q?feat(ex=C3=A9cution):=20clarifie=20Dur=C3=A9e?= =?UTF-8?q?=20et=20Temps=20r=C3=A9alis=C3=A9,=20renomme=20Dur=C3=A9e=20par?= =?UTF-8?q?=20d=C3=A9faut=20(s)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sépare la notion de Durée (paramètre configuré) de Temps réalisé (résultat mesuré au chronomètre), renomme le champ « Temps par défaut (s) » en « Durée par défaut (s) » dans la bibliothèque d'exercices, et limite l'exécution à un seul chrono visible à la fois. Le mode Score chrono est conservé tel quel comme mode du score. Co-Authored-By: Claude Opus 4.8 --- lib/application/use_cases.dart | 25 +++++- lib/domain/entities.dart | 6 ++ lib/presentation/exercise_library_screen.dart | 6 +- lib/presentation/program_screen.dart | 2 +- .../workout_execution_screen.dart | 36 +++++--- test/application/use_cases_test.dart | 89 +++++++++++++++++++ .../watch_companion_projection_test.dart | 69 +++++++++----- .../drift_repositories_test.dart | 47 ++++++++++ .../exercise_library_screen_test.dart | 12 +-- test/presentation/program_screen_test.dart | 4 +- .../workout_execution_screen_test.dart | 76 ++++++++++++++-- 11 files changed, 316 insertions(+), 56 deletions(-) diff --git a/lib/application/use_cases.dart b/lib/application/use_cases.dart index ba3ccb0..64199b0 100644 --- a/lib/application/use_cases.dart +++ b/lib/application/use_cases.dart @@ -3559,13 +3559,22 @@ final class WatchSessionProjectionProjector { final expectedPassages = _expectedPassages(snapshot); final projectedAtEpochMs = _epochMs(now); + final scoreStopwatchTimer = scoreStopwatch == null + ? null + : _scoreStopwatchTimerProjection(scoreStopwatch, now); + final setTimerProjection = setTimer == null + ? null + : _setTimerProjection(setTimer, now); + final hideScoreStopwatchTimer = _shouldHideScoreStopwatchForSetTimer( + snapshot: snapshot, + setTimerProjection: setTimerProjection, + ); final timers = [ if (activeRest != null) _restTimerProjection(activeRest, now), if (currentStep != null && currentStep.type == ExerciseStepType.time) _stepTimerProjection(stepState, currentStep, now), - if (scoreStopwatch != null) - ?_scoreStopwatchTimerProjection(scoreStopwatch, now), - if (setTimer != null) ?_setTimerProjection(setTimer, now), + ?setTimerProjection, + if (!hideScoreStopwatchTimer) ?scoreStopwatchTimer, ]; final dominantTimer = timers.isEmpty ? null : timers.first; final secondaryTimers = dominantTimer == null @@ -3846,6 +3855,16 @@ WatchTimerProjection? _setTimerProjection( ); } +bool _shouldHideScoreStopwatchForSetTimer({ + required _ResolvedExerciseSnapshot snapshot, + required WatchTimerProjection? setTimerProjection, +}) { + return snapshot.timeEnabled && + snapshot.scoreEnabled && + snapshot.scoreInputModeSnapshot == ScoreInputMode.stopwatch && + setTimerProjection != null; +} + ExerciseStep? _initialStep(_ResolvedExerciseSnapshot snapshot) { return snapshot.steps.isEmpty ? null : snapshot.steps.first; } diff --git a/lib/domain/entities.dart b/lib/domain/entities.dart index bc0fdb4..05a88d9 100644 --- a/lib/domain/entities.dart +++ b/lib/domain/entities.dart @@ -375,6 +375,12 @@ final class Exercise { defaultTargetScoreTimeMs, 'Default target score time ms', ); + _requireScoreTargetShape( + scoreEnabled: hasScoreMeasure, + scoreInputMode: scoreInputMode, + targetScore: defaultTargetScore, + targetScoreTimeMs: defaultTargetScoreTimeMs, + ); } final EntityMetadata metadata; diff --git a/lib/presentation/exercise_library_screen.dart b/lib/presentation/exercise_library_screen.dart index 1a93454..98993ce 100644 --- a/lib/presentation/exercise_library_screen.dart +++ b/lib/presentation/exercise_library_screen.dart @@ -628,7 +628,7 @@ final class _ExerciseFormScreenState extends State { TextFormField( controller: _defaultTimeController, decoration: const InputDecoration( - labelText: 'Temps par défaut (s)', + labelText: 'Durée par défaut (s)', ), keyboardType: TextInputType.number, validator: (value) => _hasTime @@ -758,7 +758,7 @@ final class _ExerciseFormScreenState extends State { TextFormField( controller: _defaultScoreTimeController, decoration: const InputDecoration( - labelText: 'Objectif de chrono par défaut (optionnel)', + labelText: 'Objectif chrono (optionnel)', ), keyboardType: const TextInputType.numberWithOptions( decimal: true, @@ -1107,7 +1107,7 @@ final class _ExerciseFormScreenState extends State { TextFormField( controller: draft.scoreTargetController, decoration: const InputDecoration( - labelText: 'Objectif de chrono par défaut (optionnel)', + labelText: 'Objectif chrono (optionnel)', ), keyboardType: const TextInputType.numberWithOptions( decimal: true, diff --git a/lib/presentation/program_screen.dart b/lib/presentation/program_screen.dart index 0304e1d..900837f 100644 --- a/lib/presentation/program_screen.dart +++ b/lib/presentation/program_screen.dart @@ -967,7 +967,7 @@ final class _ProgramExerciseCustomizationScreenState if (_draft.enabledMeasures.contains(WorkoutMeasure.score)) if (hasStopwatchScore) _NumberField( - label: 'Objectif de chrono', + label: 'Objectif chrono (optionnel)', helperText: 'Le résultat réel sera mesuré pendant la série.', initialValue: _millisecondsToSeconds( _draft.targetScoreTimeMs, diff --git a/lib/presentation/workout_execution_screen.dart b/lib/presentation/workout_execution_screen.dart index bfecb06..cf1f2a2 100644 --- a/lib/presentation/workout_execution_screen.dart +++ b/lib/presentation/workout_execution_screen.dart @@ -242,7 +242,7 @@ final class _WorkoutExecutionScreenState extends State { ), ), ], - if (_exercise.stopwatchScoreEnabled) ...[ + if (_shouldShowScoreStopwatchInput) ...[ const SizedBox(height: 8), SizedBox( height: 96, @@ -265,7 +265,7 @@ final class _WorkoutExecutionScreenState extends State { : Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (_exercise.stopwatchScoreEnabled) ...[ + if (_shouldShowScoreStopwatchInput) ...[ _ScoreStopwatchInput( elapsedLabel: _formatScoreStopwatch( Duration(milliseconds: _scoreStopwatchElapsedMs), @@ -501,6 +501,14 @@ final class _WorkoutExecutionScreenState extends State { return hasGlobalTimer && !_hasSetExecutionStarted; } + bool get _usesSetTimerForStopwatchScore { + return _exercise.timeEnabled && _exercise.stopwatchScoreEnabled; + } + + bool get _shouldShowScoreStopwatchInput { + return _exercise.stopwatchScoreEnabled && !_usesSetTimerForStopwatchScore; + } + bool get _hasSetExecutionStarted { if (_setTimer != null) { return true; @@ -1248,18 +1256,19 @@ final class _WorkoutExecutionScreenState extends State { if (!skipped && !finishWithoutChrono && _exercise.stopwatchScoreEnabled && + !_usesSetTimerForStopwatchScore && _scoreStopwatch == null && _manualScoreTimeMs == null) { final action = await showDialog<_MissingStopwatchAction>( context: context, builder: (context) => AlertDialog( - title: const Text('Chrono non lancé'), - content: const Text("Tu n’as pas démarré le chrono de cette série."), + title: const Text('Aucun temps chronométré'), + content: const Text('Tu n’as pas démarré le chrono score.'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(_MissingStopwatchAction.start), - child: const Text('Démarrer'), + child: const Text('Démarrer le chrono'), ), FilledButton( onPressed: () => Navigator.of( @@ -1311,8 +1320,9 @@ final class _WorkoutExecutionScreenState extends State { if (!skipped && _exercise.stopwatchScoreEnabled && actualScoreTimeMs == null) { - final state = _scoreStopwatch; - actualScoreTimeMs = state?.accumulatedMs; + actualScoreTimeMs = _usesSetTimerForStopwatchScore + ? setTimer?.accumulatedMs + : _scoreStopwatch?.accumulatedMs; } await widget.activeUseCases.recordCurrentSetResult( sessionId: _session.metadata.id, @@ -1747,7 +1757,7 @@ final class SetMeasureInput extends StatelessWidget { padding: const EdgeInsets.all(24), child: Column( children: [ - const Text('Temps'), + const Text('Durée'), const SizedBox(height: 8), Text( exercise.targetTimeSeconds == null @@ -1902,8 +1912,8 @@ final class _ExecutionContextHeader extends StatelessWidget { const SizedBox(height: 4), Text( setTimerComplete - ? 'Temps de série terminé : $setTimerLabel' - : 'Temps de série : $setTimerLabel', + ? 'Durée de série terminée : $setTimerLabel' + : 'Durée de série : $setTimerLabel', style: theme.textTheme.labelLarge?.copyWith( color: theme.colorScheme.primary, ), @@ -3568,7 +3578,9 @@ final class _EditSetResultSheetState extends State<_EditSetResultSheet> { if (widget.exercise.timeEnabled) ...[ TextField( controller: _timeController, - decoration: const InputDecoration(labelText: 'Temps réalisé (s)'), + decoration: const InputDecoration( + labelText: 'Durée réalisée (s)', + ), keyboardType: TextInputType.number, ), const SizedBox(height: 12), @@ -3586,7 +3598,7 @@ final class _EditSetResultSheetState extends State<_EditSetResultSheet> { controller: _scoreController, decoration: InputDecoration( labelText: widget.exercise.stopwatchScoreEnabled - ? 'Temps réalisé (chrono score, s)' + ? 'Temps réalisé (s)' : widget.exercise.scoreUnit == null ? 'Score' : 'Score (${widget.exercise.scoreUnit})', diff --git a/test/application/use_cases_test.dart b/test/application/use_cases_test.dart index e0cf883..e612a28 100644 --- a/test/application/use_cases_test.dart +++ b/test/application/use_cases_test.dart @@ -19,6 +19,95 @@ void main() { ); }); + test('Exercise rejects manual score defaults on stopwatch score mode', () { + expect( + () => Exercise( + metadata: _metadata('exercise-1'), + name: 'Sprint', + hasTimeMeasure: false, + hasRepsMeasure: false, + hasScoreMeasure: true, + scoreInputMode: ScoreInputMode.stopwatch, + defaultTargetScore: 10, + ), + throwsA(isA()), + ); + expect( + () => Exercise( + metadata: _metadata('exercise-2'), + name: 'Sprint', + hasTimeMeasure: false, + hasRepsMeasure: false, + hasScoreMeasure: true, + scoreInputMode: ScoreInputMode.stopwatch, + defaultTargetScore: 10, + defaultTargetScoreTimeMs: 12000, + ), + throwsA(isA()), + ); + }); + + test('Program exercise allows reps or time with stopwatch score', () { + final repsAndScore = ProgramExercise( + metadata: _metadata('program-exercise-1'), + programId: 'program-1', + position: 0, + exerciseNameSnapshot: 'Sprint reps', + availableTimeSnapshot: false, + availableRepsSnapshot: true, + availableScoreSnapshot: true, + scoreInputModeSnapshot: ScoreInputMode.stopwatch, + setsCount: 3, + timeEnabled: false, + repsEnabled: true, + scoreEnabled: true, + targetReps: 10, + targetScoreTimeMs: 12000, + ); + final timeAndScore = ProgramExercise( + metadata: _metadata('program-exercise-2'), + programId: 'program-1', + position: 1, + exerciseNameSnapshot: 'Sprint time', + availableTimeSnapshot: true, + availableRepsSnapshot: false, + availableScoreSnapshot: true, + scoreInputModeSnapshot: ScoreInputMode.stopwatch, + setsCount: 3, + timeEnabled: true, + repsEnabled: false, + scoreEnabled: true, + targetTimeSeconds: 30, + targetScoreTimeMs: 12000, + ); + + expect(repsAndScore.scoreInputModeSnapshot, ScoreInputMode.stopwatch); + expect(timeAndScore.scoreInputModeSnapshot, ScoreInputMode.stopwatch); + }); + + test('Program exercise rejects manual score target on stopwatch score', () { + expect( + () => ProgramExercise( + metadata: _metadata('program-exercise-1'), + programId: 'program-1', + position: 0, + exerciseNameSnapshot: 'Sprint score', + availableTimeSnapshot: false, + availableRepsSnapshot: true, + availableScoreSnapshot: true, + scoreInputModeSnapshot: ScoreInputMode.stopwatch, + setsCount: 3, + timeEnabled: false, + repsEnabled: true, + scoreEnabled: true, + targetReps: 10, + targetScore: 10, + targetScoreTimeMs: 12000, + ), + throwsA(isA()), + ); + }); + test( 'Exercise use case rejects active measures without default targets', () async { diff --git a/test/application/watch_companion_projection_test.dart b/test/application/watch_companion_projection_test.dart index 629fc6a..a4d4e76 100644 --- a/test/application/watch_companion_projection_test.dart +++ b/test/application/watch_companion_projection_test.dart @@ -40,23 +40,61 @@ void main() { expect(projection.primaryAction, WatchPrimaryAction.startCurrentExercise); }); + test('projects running with duration timer before stopwatch score', () async { + final now = DateTime.utc(2026, 7, 25, 12); + final session = _session( + timeEnabled: true, + scoreEnabled: true, + scoreInputMode: ScoreInputMode.stopwatch, + steps: [_step(defaultTargetValue: 30)], + ); + final repository = _FakeActiveSessionRepository() + ..session = session + ..stepProgressStates['step-state'] = _stepState( + sessionId: session.metadata.id, + status: ActiveExerciseStepProgressStatus.runningTimer, + startedAt: now.subtract(const Duration(seconds: 5)), + ) + ..scoreStopwatchStates['score'] = _scoreStopwatch( + sessionId: session.metadata.id, + startedAt: now.subtract(const Duration(seconds: 4)), + ) + ..setTimerStates['set'] = _setTimer( + sessionId: session.metadata.id, + startedAt: now.subtract(const Duration(seconds: 6)), + ); + final projector = _projector(repository, _clock(now)); + + final projection = await projector.project(revision: 1); + + expect(projection.phase, WatchSessionPhase.running); + expect(projection.dominantTimer?.kind, WatchTimerKind.step); + expect(projection.dominantTimer?.accumulatedMs, 0); + expect( + projection.dominantTimer?.startedAtEpochMs, + now.subtract(const Duration(seconds: 5)).millisecondsSinceEpoch, + ); + expect(projection.secondaryTimers.map((timer) => timer.kind), [ + WatchTimerKind.setTimer, + ]); + expect(projection.primaryAction, WatchPrimaryAction.pauseSession); + expect( + projection.secondaryActions, + contains(WatchSecondaryAction.finishCurrentSet), + ); + }); + test( - 'projects running with dominant step timer and secondary timers', + 'projects duration and stopwatch score with only duration timer visible', () async { final now = DateTime.utc(2026, 7, 25, 12); final session = _session( timeEnabled: true, scoreEnabled: true, scoreInputMode: ScoreInputMode.stopwatch, - steps: [_step(defaultTargetValue: 30)], ); final repository = _FakeActiveSessionRepository() ..session = session - ..stepProgressStates['step-state'] = _stepState( - sessionId: session.metadata.id, - status: ActiveExerciseStepProgressStatus.runningTimer, - startedAt: now.subtract(const Duration(seconds: 5)), - ) ..scoreStopwatchStates['score'] = _scoreStopwatch( sessionId: session.metadata.id, startedAt: now.subtract(const Duration(seconds: 4)), @@ -70,21 +108,8 @@ void main() { final projection = await projector.project(revision: 1); expect(projection.phase, WatchSessionPhase.running); - expect(projection.dominantTimer?.kind, WatchTimerKind.step); - expect(projection.dominantTimer?.accumulatedMs, 0); - expect( - projection.dominantTimer?.startedAtEpochMs, - now.subtract(const Duration(seconds: 5)).millisecondsSinceEpoch, - ); - expect(projection.secondaryTimers.map((timer) => timer.kind), [ - WatchTimerKind.scoreStopwatch, - WatchTimerKind.setTimer, - ]); - expect(projection.primaryAction, WatchPrimaryAction.pauseSession); - expect( - projection.secondaryActions, - contains(WatchSecondaryAction.finishCurrentSet), - ); + expect(projection.dominantTimer?.kind, WatchTimerKind.setTimer); + expect(projection.secondaryTimers, isEmpty); }, ); diff --git a/test/infrastructure/drift_repositories_test.dart b/test/infrastructure/drift_repositories_test.dart index 3a81daf..17b59f0 100644 --- a/test/infrastructure/drift_repositories_test.dart +++ b/test/infrastructure/drift_repositories_test.dart @@ -1532,6 +1532,53 @@ void main() { }, ); + test( + 'active set result persists duration and stopwatch score separately', + () async { + final now = DateTime.utc(2026, 7, 17, 12); + await activeRepository.save( + ActiveWorkoutSession( + metadata: _metadata('session-chrono', now), + status: ActiveWorkoutStatus.running, + startedAt: now, + lastPersistedAt: now, + elapsedActiveMs: 0, + currentProgramIndex: 0, + currentExerciseIndex: 0, + currentSetIndex: 0, + resolvedTemplateSnapshotJson: _resolvedSnapshot(), + ), + ); + await activeRepository.saveSetResult( + ActiveSetResult( + metadata: _metadata('active-result-chrono', now), + activeWorkoutSessionId: 'session-chrono', + programSnapshotId: 'program-snapshot-1', + exerciseSnapshotId: 'exercise-snapshot-1', + programIndex: 0, + exerciseIndex: 0, + setIndex: 0, + actualTimeMs: 30000, + actualScoreTimeMs: 12000, + scoreInputModeSnapshot: ScoreInputMode.stopwatch, + completedAt: now, + ), + ); + + final afterAppKillRepository = local.DriftActiveSessionRepository( + database, + ); + final restored = await afterAppKillRepository.listSetResults( + 'session-chrono', + ); + + expect(restored, hasLength(1)); + expect(restored.single.actualTimeMs, 30000); + expect(restored.single.actualScoreTimeMs, 12000); + expect(restored.single.scoreInputModeSnapshot, ScoreInputMode.stopwatch); + }, + ); + test( 'closing a session stores autonomous history rows with set snapshots', () async { diff --git a/test/presentation/exercise_library_screen_test.dart b/test/presentation/exercise_library_screen_test.dart index f7163e2..f02c84b 100644 --- a/test/presentation/exercise_library_screen_test.dart +++ b/test/presentation/exercise_library_screen_test.dart @@ -107,13 +107,13 @@ void main() { expect(find.widgetWithText(Chip, 'match'), findsOneWidget); await tester.dragUntilVisible( - find.widgetWithText(TextFormField, 'Temps par défaut (s)'), + find.widgetWithText(TextFormField, 'Durée par défaut (s)'), find.byType(ListView), const Offset(0, -200), ); await tester.pumpAndSettle(); await tester.enterText( - find.widgetWithText(TextFormField, 'Temps par défaut (s)'), + find.widgetWithText(TextFormField, 'Durée par défaut (s)'), '30', ); await tester.dragUntilVisible( @@ -195,7 +195,7 @@ void main() { await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Run'); await tester.enterText( - find.widgetWithText(TextFormField, 'Temps par défaut (s)'), + find.widgetWithText(TextFormField, 'Durée par défaut (s)'), '30', ); await tester.tap(find.widgetWithText(SwitchListTile, 'Score')); @@ -510,7 +510,7 @@ void main() { await _pumpExerciseForm(tester, exerciseRepository); await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Combo'); await tester.enterText( - find.widgetWithText(TextFormField, 'Temps par défaut (s)'), + find.widgetWithText(TextFormField, 'Durée par défaut (s)'), '30', ); await tester.tap( @@ -557,7 +557,7 @@ void main() { 'Combo', ); await tester.enterText( - find.widgetWithText(TextFormField, 'Temps par défaut (s)'), + find.widgetWithText(TextFormField, 'Durée par défaut (s)'), '30', ); await tester.tap( @@ -621,7 +621,7 @@ void main() { await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Squat'); await tester.enterText( - find.widgetWithText(TextFormField, 'Temps par défaut (s)'), + find.widgetWithText(TextFormField, 'Durée par défaut (s)'), '45', ); await tester.tap(find.text('Ajouter une image')); diff --git a/test/presentation/program_screen_test.dart b/test/presentation/program_screen_test.dart index d597d9a..0392517 100644 --- a/test/presentation/program_screen_test.dart +++ b/test/presentation/program_screen_test.dart @@ -707,12 +707,12 @@ void main() { findsOneWidget, ); await tester.dragUntilVisible( - find.text('Objectif de chrono'), + find.text('Objectif chrono (optionnel)'), find.byType(ListView), const Offset(0, -200), ); expect( - find.widgetWithText(TextFormField, 'Objectif de chrono'), + find.widgetWithText(TextFormField, 'Objectif chrono (optionnel)'), findsOneWidget, ); expect(find.widgetWithText(TextFormField, 'Cible score'), findsNothing); diff --git a/test/presentation/workout_execution_screen_test.dart b/test/presentation/workout_execution_screen_test.dart index 9c333d7..98840e1 100644 --- a/test/presentation/workout_execution_screen_test.dart +++ b/test/presentation/workout_execution_screen_test.dart @@ -41,7 +41,7 @@ void main() { ); expect(find.text('Squat'), findsOneWidget); - expect(find.text('Temps de série : 00:45'), findsOneWidget); + expect(find.text('Durée de série : 00:45'), findsOneWidget); expect(find.widgetWithText(FilledButton, 'Démarrer'), findsOneWidget); }); @@ -141,7 +141,7 @@ void main() { await tester.pump(); await tester.pump(); - expect(find.text('Temps de série : 05:00'), findsOneWidget); + expect(find.text('Durée de série : 05:00'), findsOneWidget); expect(find.text('1:12 · 00:45.1'), findsOneWidget); expect(find.text('00:42.8'), findsOneWidget); expect(find.textContaining('72000'), findsNothing); @@ -176,7 +176,7 @@ void main() { ), ); - final timeY = tester.getTopLeft(find.text('Temps')).dy; + final timeY = tester.getTopLeft(find.text('Durée')).dy; final repsY = tester.getTopLeft(find.text('Répétitions')).dy; final scoreY = tester.getTopLeft(find.text('Score (kg)')).dy; @@ -426,6 +426,7 @@ void main() { currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot( + timeEnabled: false, scoreInputMode: ScoreInputMode.stopwatch, ), ); @@ -492,6 +493,7 @@ void main() { currentExerciseIndex: 0, currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot( + timeEnabled: false, scoreInputMode: ScoreInputMode.stopwatch, ), ); @@ -512,12 +514,12 @@ void main() { await tester.tap(find.text('Terminer la série')); await tester.pumpAndSettle(); - expect(find.text('Chrono non lancé'), findsOneWidget); + expect(find.text('Aucun temps chronométré'), findsOneWidget); + expect(find.text('Tu n’as pas démarré le chrono score.'), findsOneWidget); expect( - find.text('Tu n’as pas démarré le chrono de cette série.'), + find.widgetWithText(TextButton, 'Démarrer le chrono'), findsOneWidget, ); - expect(find.widgetWithText(TextButton, 'Démarrer'), findsOneWidget); expect(find.text('Terminer sans chrono'), findsOneWidget); expect(repository.results, isEmpty); }); @@ -542,6 +544,7 @@ void main() { currentSetIndex: 0, resolvedTemplateSnapshotJson: _sessionSnapshot( setsCount: 2, + timeEnabled: false, scoreInputMode: ScoreInputMode.stopwatch, ), ); @@ -736,6 +739,65 @@ void main() { expect(repository.results.single.actualTimeMs, 2000); }); + testWidgets( + 'durée et score chrono utilisent un seul chrono visible en exécution', + (tester) async { + await tester.binding.setSurfaceSize(const Size(400, 1400)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + 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, + timeEnabled: true, + scoreInputMode: ScoreInputMode.stopwatch, + targetTimeSeconds: 30, + ), + ); + 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('Chrono score'), findsNothing); + expect(find.text('Durée de série : 00:30'), findsOneWidget); + + await tester.tap(find.widgetWithText(FilledButton, 'Démarrer').first); + await tester.pump(); + clock.value = DateTime.utc(2026, 7, 17, 12, 0, 2); + await tester.pump(const Duration(seconds: 2)); + + expect(find.text('Chrono score'), findsNothing); + expect(find.text('Durée de série : 00:02'), findsOneWidget); + + await tester.tap(find.text('Terminer la série')); + await tester.pump(); + + final result = repository.results.single; + expect(result.actualTimeMs, 2000); + expect(result.actualScoreTimeMs, 2000); + }, + ); + testWidgets( 'terminer la série 2 avec temps répétitions et score manuel avance', (tester) async { @@ -780,7 +842,7 @@ void main() { expect(find.text('Programme jambes'), findsOneWidget); expect(find.text('2 / 3'), findsOneWidget); expect(find.text('Squat'), findsOneWidget); - expect(find.text('Temps de série : 00:30'), findsOneWidget); + expect(find.text('Durée de série : 00:30'), findsOneWidget); await tester.tap(find.widgetWithText(FilledButton, 'Démarrer').first); await tester.pump();