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

@ -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 {

View File

@ -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);
},
);