feat(exécution): clarifie Durée et Temps réalisé, renomme Durée par défaut (s)

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 09:37:44 +02:00
parent 6d86700348
commit e3c7ca0dab
11 changed files with 316 additions and 56 deletions

View File

@ -3559,13 +3559,22 @@ final class WatchSessionProjectionProjector {
final expectedPassages = _expectedPassages(snapshot); final expectedPassages = _expectedPassages(snapshot);
final projectedAtEpochMs = _epochMs(now); 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 = <WatchTimerProjection>[ final timers = <WatchTimerProjection>[
if (activeRest != null) _restTimerProjection(activeRest, now), if (activeRest != null) _restTimerProjection(activeRest, now),
if (currentStep != null && currentStep.type == ExerciseStepType.time) if (currentStep != null && currentStep.type == ExerciseStepType.time)
_stepTimerProjection(stepState, currentStep, now), _stepTimerProjection(stepState, currentStep, now),
if (scoreStopwatch != null) ?setTimerProjection,
?_scoreStopwatchTimerProjection(scoreStopwatch, now), if (!hideScoreStopwatchTimer) ?scoreStopwatchTimer,
if (setTimer != null) ?_setTimerProjection(setTimer, now),
]; ];
final dominantTimer = timers.isEmpty ? null : timers.first; final dominantTimer = timers.isEmpty ? null : timers.first;
final secondaryTimers = dominantTimer == null 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) { ExerciseStep? _initialStep(_ResolvedExerciseSnapshot snapshot) {
return snapshot.steps.isEmpty ? null : snapshot.steps.first; return snapshot.steps.isEmpty ? null : snapshot.steps.first;
} }

View File

@ -375,6 +375,12 @@ final class Exercise {
defaultTargetScoreTimeMs, defaultTargetScoreTimeMs,
'Default target score time ms', 'Default target score time ms',
); );
_requireScoreTargetShape(
scoreEnabled: hasScoreMeasure,
scoreInputMode: scoreInputMode,
targetScore: defaultTargetScore,
targetScoreTimeMs: defaultTargetScoreTimeMs,
);
} }
final EntityMetadata metadata; final EntityMetadata metadata;

View File

@ -628,7 +628,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
TextFormField( TextFormField(
controller: _defaultTimeController, controller: _defaultTimeController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Temps par défaut (s)', labelText: 'Durée par défaut (s)',
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
validator: (value) => _hasTime validator: (value) => _hasTime
@ -758,7 +758,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
TextFormField( TextFormField(
controller: _defaultScoreTimeController, controller: _defaultScoreTimeController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Objectif de chrono par défaut (optionnel)', labelText: 'Objectif chrono (optionnel)',
), ),
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
decimal: true, decimal: true,
@ -1107,7 +1107,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
TextFormField( TextFormField(
controller: draft.scoreTargetController, controller: draft.scoreTargetController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Objectif de chrono par défaut (optionnel)', labelText: 'Objectif chrono (optionnel)',
), ),
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
decimal: true, decimal: true,

View File

@ -967,7 +967,7 @@ final class _ProgramExerciseCustomizationScreenState
if (_draft.enabledMeasures.contains(WorkoutMeasure.score)) if (_draft.enabledMeasures.contains(WorkoutMeasure.score))
if (hasStopwatchScore) if (hasStopwatchScore)
_NumberField( _NumberField(
label: 'Objectif de chrono', label: 'Objectif chrono (optionnel)',
helperText: 'Le résultat réel sera mesuré pendant la série.', helperText: 'Le résultat réel sera mesuré pendant la série.',
initialValue: _millisecondsToSeconds( initialValue: _millisecondsToSeconds(
_draft.targetScoreTimeMs, _draft.targetScoreTimeMs,

View File

@ -242,7 +242,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
), ),
), ),
], ],
if (_exercise.stopwatchScoreEnabled) ...[ if (_shouldShowScoreStopwatchInput) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
SizedBox( SizedBox(
height: 96, height: 96,
@ -265,7 +265,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
: Column( : Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (_exercise.stopwatchScoreEnabled) ...[ if (_shouldShowScoreStopwatchInput) ...[
_ScoreStopwatchInput( _ScoreStopwatchInput(
elapsedLabel: _formatScoreStopwatch( elapsedLabel: _formatScoreStopwatch(
Duration(milliseconds: _scoreStopwatchElapsedMs), Duration(milliseconds: _scoreStopwatchElapsedMs),
@ -501,6 +501,14 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
return hasGlobalTimer && !_hasSetExecutionStarted; return hasGlobalTimer && !_hasSetExecutionStarted;
} }
bool get _usesSetTimerForStopwatchScore {
return _exercise.timeEnabled && _exercise.stopwatchScoreEnabled;
}
bool get _shouldShowScoreStopwatchInput {
return _exercise.stopwatchScoreEnabled && !_usesSetTimerForStopwatchScore;
}
bool get _hasSetExecutionStarted { bool get _hasSetExecutionStarted {
if (_setTimer != null) { if (_setTimer != null) {
return true; return true;
@ -1248,18 +1256,19 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
if (!skipped && if (!skipped &&
!finishWithoutChrono && !finishWithoutChrono &&
_exercise.stopwatchScoreEnabled && _exercise.stopwatchScoreEnabled &&
!_usesSetTimerForStopwatchScore &&
_scoreStopwatch == null && _scoreStopwatch == null &&
_manualScoreTimeMs == null) { _manualScoreTimeMs == null) {
final action = await showDialog<_MissingStopwatchAction>( final action = await showDialog<_MissingStopwatchAction>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Chrono non lancé'), title: const Text('Aucun temps chronométré'),
content: const Text("Tu nas pas démarré le chrono de cette série."), content: const Text('Tu nas pas démarré le chrono score.'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => onPressed: () =>
Navigator.of(context).pop(_MissingStopwatchAction.start), Navigator.of(context).pop(_MissingStopwatchAction.start),
child: const Text('Démarrer'), child: const Text('Démarrer le chrono'),
), ),
FilledButton( FilledButton(
onPressed: () => Navigator.of( onPressed: () => Navigator.of(
@ -1311,8 +1320,9 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
if (!skipped && if (!skipped &&
_exercise.stopwatchScoreEnabled && _exercise.stopwatchScoreEnabled &&
actualScoreTimeMs == null) { actualScoreTimeMs == null) {
final state = _scoreStopwatch; actualScoreTimeMs = _usesSetTimerForStopwatchScore
actualScoreTimeMs = state?.accumulatedMs; ? setTimer?.accumulatedMs
: _scoreStopwatch?.accumulatedMs;
} }
await widget.activeUseCases.recordCurrentSetResult( await widget.activeUseCases.recordCurrentSetResult(
sessionId: _session.metadata.id, sessionId: _session.metadata.id,
@ -1747,7 +1757,7 @@ final class SetMeasureInput extends StatelessWidget {
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Column( child: Column(
children: [ children: [
const Text('Temps'), const Text('Durée'),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
exercise.targetTimeSeconds == null exercise.targetTimeSeconds == null
@ -1902,8 +1912,8 @@ final class _ExecutionContextHeader extends StatelessWidget {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
setTimerComplete setTimerComplete
? 'Temps de série terminé : $setTimerLabel' ? 'Durée de série terminée : $setTimerLabel'
: 'Temps de série : $setTimerLabel', : 'Durée de série : $setTimerLabel',
style: theme.textTheme.labelLarge?.copyWith( style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
@ -3568,7 +3578,9 @@ final class _EditSetResultSheetState extends State<_EditSetResultSheet> {
if (widget.exercise.timeEnabled) ...[ if (widget.exercise.timeEnabled) ...[
TextField( TextField(
controller: _timeController, controller: _timeController,
decoration: const InputDecoration(labelText: 'Temps réalisé (s)'), decoration: const InputDecoration(
labelText: 'Durée réalisée (s)',
),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@ -3586,7 +3598,7 @@ final class _EditSetResultSheetState extends State<_EditSetResultSheet> {
controller: _scoreController, controller: _scoreController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: widget.exercise.stopwatchScoreEnabled labelText: widget.exercise.stopwatchScoreEnabled
? 'Temps réalisé (chrono score, s)' ? 'Temps réalisé (s)'
: widget.exercise.scoreUnit == null : widget.exercise.scoreUnit == null
? 'Score' ? 'Score'
: 'Score (${widget.exercise.scoreUnit})', : 'Score (${widget.exercise.scoreUnit})',

View File

@ -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<DomainException>()),
);
expect(
() => Exercise(
metadata: _metadata('exercise-2'),
name: 'Sprint',
hasTimeMeasure: false,
hasRepsMeasure: false,
hasScoreMeasure: true,
scoreInputMode: ScoreInputMode.stopwatch,
defaultTargetScore: 10,
defaultTargetScoreTimeMs: 12000,
),
throwsA(isA<DomainException>()),
);
});
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<DomainException>()),
);
});
test( test(
'Exercise use case rejects active measures without default targets', 'Exercise use case rejects active measures without default targets',
() async { () async {

View File

@ -40,9 +40,7 @@ void main() {
expect(projection.primaryAction, WatchPrimaryAction.startCurrentExercise); expect(projection.primaryAction, WatchPrimaryAction.startCurrentExercise);
}); });
test( test('projects running with duration timer before stopwatch score', () async {
'projects running with dominant step timer and secondary timers',
() async {
final now = DateTime.utc(2026, 7, 25, 12); final now = DateTime.utc(2026, 7, 25, 12);
final session = _session( final session = _session(
timeEnabled: true, timeEnabled: true,
@ -77,7 +75,6 @@ void main() {
now.subtract(const Duration(seconds: 5)).millisecondsSinceEpoch, now.subtract(const Duration(seconds: 5)).millisecondsSinceEpoch,
); );
expect(projection.secondaryTimers.map((timer) => timer.kind), [ expect(projection.secondaryTimers.map((timer) => timer.kind), [
WatchTimerKind.scoreStopwatch,
WatchTimerKind.setTimer, WatchTimerKind.setTimer,
]); ]);
expect(projection.primaryAction, WatchPrimaryAction.pauseSession); expect(projection.primaryAction, WatchPrimaryAction.pauseSession);
@ -85,6 +82,34 @@ void main() {
projection.secondaryActions, projection.secondaryActions,
contains(WatchSecondaryAction.finishCurrentSet), contains(WatchSecondaryAction.finishCurrentSet),
); );
});
test(
'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,
);
final repository = _FakeActiveSessionRepository()
..session = session
..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.setTimer);
expect(projection.secondaryTimers, isEmpty);
}, },
); );

View File

@ -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( test(
'closing a session stores autonomous history rows with set snapshots', 'closing a session stores autonomous history rows with set snapshots',
() async { () async {

View File

@ -107,13 +107,13 @@ void main() {
expect(find.widgetWithText(Chip, 'match'), findsOneWidget); expect(find.widgetWithText(Chip, 'match'), findsOneWidget);
await tester.dragUntilVisible( await tester.dragUntilVisible(
find.widgetWithText(TextFormField, 'Temps par défaut (s)'), find.widgetWithText(TextFormField, 'Durée par défaut (s)'),
find.byType(ListView), find.byType(ListView),
const Offset(0, -200), const Offset(0, -200),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.enterText( await tester.enterText(
find.widgetWithText(TextFormField, 'Temps par défaut (s)'), find.widgetWithText(TextFormField, 'Durée par défaut (s)'),
'30', '30',
); );
await tester.dragUntilVisible( await tester.dragUntilVisible(
@ -195,7 +195,7 @@ void main() {
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Run'); await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Run');
await tester.enterText( await tester.enterText(
find.widgetWithText(TextFormField, 'Temps par défaut (s)'), find.widgetWithText(TextFormField, 'Durée par défaut (s)'),
'30', '30',
); );
await tester.tap(find.widgetWithText(SwitchListTile, 'Score')); await tester.tap(find.widgetWithText(SwitchListTile, 'Score'));
@ -510,7 +510,7 @@ void main() {
await _pumpExerciseForm(tester, exerciseRepository); await _pumpExerciseForm(tester, exerciseRepository);
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Combo'); await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Combo');
await tester.enterText( await tester.enterText(
find.widgetWithText(TextFormField, 'Temps par défaut (s)'), find.widgetWithText(TextFormField, 'Durée par défaut (s)'),
'30', '30',
); );
await tester.tap( await tester.tap(
@ -557,7 +557,7 @@ void main() {
'Combo', 'Combo',
); );
await tester.enterText( await tester.enterText(
find.widgetWithText(TextFormField, 'Temps par défaut (s)'), find.widgetWithText(TextFormField, 'Durée par défaut (s)'),
'30', '30',
); );
await tester.tap( await tester.tap(
@ -621,7 +621,7 @@ void main() {
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Squat'); await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Squat');
await tester.enterText( await tester.enterText(
find.widgetWithText(TextFormField, 'Temps par défaut (s)'), find.widgetWithText(TextFormField, 'Durée par défaut (s)'),
'45', '45',
); );
await tester.tap(find.text('Ajouter une image')); await tester.tap(find.text('Ajouter une image'));

View File

@ -707,12 +707,12 @@ void main() {
findsOneWidget, findsOneWidget,
); );
await tester.dragUntilVisible( await tester.dragUntilVisible(
find.text('Objectif de chrono'), find.text('Objectif chrono (optionnel)'),
find.byType(ListView), find.byType(ListView),
const Offset(0, -200), const Offset(0, -200),
); );
expect( expect(
find.widgetWithText(TextFormField, 'Objectif de chrono'), find.widgetWithText(TextFormField, 'Objectif chrono (optionnel)'),
findsOneWidget, findsOneWidget,
); );
expect(find.widgetWithText(TextFormField, 'Cible score'), findsNothing); expect(find.widgetWithText(TextFormField, 'Cible score'), findsNothing);

View File

@ -41,7 +41,7 @@ void main() {
); );
expect(find.text('Squat'), findsOneWidget); 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); expect(find.widgetWithText(FilledButton, 'Démarrer'), findsOneWidget);
}); });
@ -141,7 +141,7 @@ void main() {
await tester.pump(); await tester.pump();
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('1:12 · 00:45.1'), findsOneWidget);
expect(find.text('00:42.8'), findsOneWidget); expect(find.text('00:42.8'), findsOneWidget);
expect(find.textContaining('72000'), findsNothing); 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 repsY = tester.getTopLeft(find.text('Répétitions')).dy;
final scoreY = tester.getTopLeft(find.text('Score (kg)')).dy; final scoreY = tester.getTopLeft(find.text('Score (kg)')).dy;
@ -426,6 +426,7 @@ void main() {
currentExerciseIndex: 0, currentExerciseIndex: 0,
currentSetIndex: 0, currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot( resolvedTemplateSnapshotJson: _sessionSnapshot(
timeEnabled: false,
scoreInputMode: ScoreInputMode.stopwatch, scoreInputMode: ScoreInputMode.stopwatch,
), ),
); );
@ -492,6 +493,7 @@ void main() {
currentExerciseIndex: 0, currentExerciseIndex: 0,
currentSetIndex: 0, currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot( resolvedTemplateSnapshotJson: _sessionSnapshot(
timeEnabled: false,
scoreInputMode: ScoreInputMode.stopwatch, scoreInputMode: ScoreInputMode.stopwatch,
), ),
); );
@ -512,12 +514,12 @@ void main() {
await tester.tap(find.text('Terminer la série')); await tester.tap(find.text('Terminer la série'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Chrono non lancé'), findsOneWidget); expect(find.text('Aucun temps chronométré'), findsOneWidget);
expect(find.text('Tu nas pas démarré le chrono score.'), findsOneWidget);
expect( expect(
find.text('Tu nas pas démarré le chrono de cette série.'), find.widgetWithText(TextButton, 'Démarrer le chrono'),
findsOneWidget, findsOneWidget,
); );
expect(find.widgetWithText(TextButton, 'Démarrer'), findsOneWidget);
expect(find.text('Terminer sans chrono'), findsOneWidget); expect(find.text('Terminer sans chrono'), findsOneWidget);
expect(repository.results, isEmpty); expect(repository.results, isEmpty);
}); });
@ -542,6 +544,7 @@ void main() {
currentSetIndex: 0, currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot( resolvedTemplateSnapshotJson: _sessionSnapshot(
setsCount: 2, setsCount: 2,
timeEnabled: false,
scoreInputMode: ScoreInputMode.stopwatch, scoreInputMode: ScoreInputMode.stopwatch,
), ),
); );
@ -736,6 +739,65 @@ void main() {
expect(repository.results.single.actualTimeMs, 2000); 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( testWidgets(
'terminer la série 2 avec temps répétitions et score manuel avance', 'terminer la série 2 avec temps répétitions et score manuel avance',
(tester) async { (tester) async {
@ -780,7 +842,7 @@ void main() {
expect(find.text('Programme jambes'), findsOneWidget); expect(find.text('Programme jambes'), findsOneWidget);
expect(find.text('2 / 3'), findsOneWidget); expect(find.text('2 / 3'), findsOneWidget);
expect(find.text('Squat'), 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.tap(find.widgetWithText(FilledButton, 'Démarrer').first);
await tester.pump(); await tester.pump();