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:
@ -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 = <WatchTimerProjection>[
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -628,7 +628,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
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<ExerciseFormScreen> {
|
||||
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<ExerciseFormScreen> {
|
||||
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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -242,7 +242,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_exercise.stopwatchScoreEnabled) ...[
|
||||
if (_shouldShowScoreStopwatchInput) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 96,
|
||||
@ -265,7 +265,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
: 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<WorkoutExecutionScreen> {
|
||||
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<WorkoutExecutionScreen> {
|
||||
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<WorkoutExecutionScreen> {
|
||||
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})',
|
||||
|
||||
@ -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(
|
||||
'Exercise use case rejects active measures without default targets',
|
||||
() async {
|
||||
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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'));
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
Reference in New Issue
Block a user