feat(exécution): refonte des chronos de séance et carte exercice actif (ticket #90)
Réorganise l'écran d'exécution autour d'une carte "Exercice actif" sous le compteur SÉRIE X/Y (nom, médias, temps de série, action "Démarrer l'exercice"). Le timer de série devient un état persistant dédié (plus un DateTime UI volatile), pause-aware, et "Démarrer l'exercice" lance en une action tous les chronos qui doivent démarrer en début de série (série, première étape temps, score chrono si stopwatch). Implémentation restée non commitée depuis sa réalisation ; QA a validé les vérifications statiques exécutables en sandbox (dart analyze, git diff --check). flutter analyze / flutter test restent à relancer dans un environnement avec cache Flutter SDK accessible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -258,6 +258,196 @@ void main() {
|
||||
expect(restored.steps[2].defaultTargetScoreTimeMs, 12000);
|
||||
});
|
||||
|
||||
group('exercise repository round-trips exercise option combinations', () {
|
||||
final now = DateTime.utc(2026, 7, 17, 12);
|
||||
final cases = <_ExerciseRoundTripCase>[
|
||||
_ExerciseRoundTripCase(
|
||||
label: 'reps without score or steps',
|
||||
exercise: Exercise(
|
||||
metadata: _metadata('exercise-combo-reps', now),
|
||||
name: 'Pompes',
|
||||
description: 'Simple reps target',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 12,
|
||||
),
|
||||
),
|
||||
_ExerciseRoundTripCase(
|
||||
label: 'reps without score and with steps',
|
||||
exercise: Exercise(
|
||||
metadata: _metadata('exercise-combo-reps-steps', now),
|
||||
name: 'Complexe poids du corps',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
defaultTargetReps: 1,
|
||||
steps: [
|
||||
_exerciseStep(
|
||||
id: 'combo-reps-step-1',
|
||||
position: 0,
|
||||
name: 'Pompes',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 10,
|
||||
),
|
||||
_exerciseStep(
|
||||
id: 'combo-reps-step-2',
|
||||
position: 1,
|
||||
name: 'Gainage',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 30,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ExerciseRoundTripCase(
|
||||
label: 'manual score without steps',
|
||||
exercise: Exercise(
|
||||
metadata: _metadata('exercise-combo-manual-score', now),
|
||||
name: 'Charge max',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: false,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Charge',
|
||||
scoreUnit: 'kg',
|
||||
defaultTargetScore: 0,
|
||||
),
|
||||
),
|
||||
_ExerciseRoundTripCase(
|
||||
label: 'manual score with unscored steps',
|
||||
exercise: Exercise(
|
||||
metadata: _metadata('exercise-combo-manual-score-steps', now),
|
||||
name: 'Technique haltères',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Charge',
|
||||
scoreUnit: 'kg',
|
||||
defaultTargetReps: 8,
|
||||
defaultTargetScore: 12.5,
|
||||
steps: [
|
||||
_exerciseStep(
|
||||
id: 'combo-manual-score-step-1',
|
||||
position: 0,
|
||||
name: 'Installation',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 15,
|
||||
),
|
||||
_exerciseStep(
|
||||
id: 'combo-manual-score-step-2',
|
||||
position: 1,
|
||||
name: 'Serie',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ExerciseRoundTripCase(
|
||||
label: 'manual exercise score with manual scored steps',
|
||||
exercise: Exercise(
|
||||
metadata: _metadata('exercise-combo-manual-step-score', now),
|
||||
name: 'Circuit precision',
|
||||
hasTimeMeasure: true,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Qualite',
|
||||
scoreUnit: 'pts',
|
||||
defaultTargetTimeSeconds: 45,
|
||||
defaultTargetReps: 10,
|
||||
defaultTargetScore: 80,
|
||||
autoStartNextTimedStep: false,
|
||||
steps: [
|
||||
_exerciseStep(
|
||||
id: 'combo-manual-step-score-1',
|
||||
position: 0,
|
||||
name: 'Bloc reps',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 10,
|
||||
hasScore: true,
|
||||
scoreLabel: 'Amplitude',
|
||||
scoreUnit: 'pts',
|
||||
defaultTargetScore: 0,
|
||||
),
|
||||
_exerciseStep(
|
||||
id: 'combo-manual-step-score-2',
|
||||
position: 1,
|
||||
name: 'Bloc temps',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 35,
|
||||
hasScore: true,
|
||||
scoreLabel: 'Tenue',
|
||||
scoreUnit: 's',
|
||||
defaultTargetScore: 35,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ExerciseRoundTripCase(
|
||||
label: 'stopwatch score without steps',
|
||||
exercise: Exercise(
|
||||
metadata: _metadata('exercise-combo-stopwatch-score', now),
|
||||
name: 'Sprint chrono',
|
||||
hasTimeMeasure: true,
|
||||
hasRepsMeasure: false,
|
||||
hasScoreMeasure: true,
|
||||
scoreInputMode: ScoreInputMode.stopwatch,
|
||||
defaultTargetTimeSeconds: 20,
|
||||
defaultTargetScoreTimeMs: 11500,
|
||||
),
|
||||
),
|
||||
_ExerciseRoundTripCase(
|
||||
label: 'stopwatch score with stopwatch scored steps',
|
||||
exercise: Exercise(
|
||||
metadata: _metadata('exercise-combo-stopwatch-score-steps', now),
|
||||
name: 'Sprint fractionne',
|
||||
imageMediaIds: const ['media-start', 'media-finish'],
|
||||
iconMediaId: 'media-start',
|
||||
videoMediaId: 'video-demo',
|
||||
hasTimeMeasure: true,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreInputMode: ScoreInputMode.stopwatch,
|
||||
defaultTargetTimeSeconds: 60,
|
||||
defaultTargetReps: 4,
|
||||
defaultTargetScoreTimeMs: 42000,
|
||||
steps: [
|
||||
_exerciseStep(
|
||||
id: 'combo-stopwatch-step-1',
|
||||
position: 0,
|
||||
name: 'Acceleration',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 10,
|
||||
hasScore: true,
|
||||
scoreInputMode: ScoreInputMode.stopwatch,
|
||||
defaultTargetScoreTimeMs: 9500,
|
||||
),
|
||||
_exerciseStep(
|
||||
id: 'combo-stopwatch-step-2',
|
||||
position: 1,
|
||||
name: 'Recuperation active',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
for (final roundTripCase in cases) {
|
||||
test(roundTripCase.label, () async {
|
||||
await exerciseRepository.save(roundTripCase.exercise);
|
||||
|
||||
final restored = await exerciseRepository.findById(
|
||||
roundTripCase.exercise.metadata.id,
|
||||
);
|
||||
|
||||
expect(restored, isNotNull);
|
||||
_expectExerciseEquals(restored!, roundTripCase.exercise);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'program exercise step snapshot is preserved when source exercise changes',
|
||||
() async {
|
||||
@ -578,6 +768,94 @@ ProgramExercise _programExercise(
|
||||
);
|
||||
}
|
||||
|
||||
ExerciseStep _exerciseStep({
|
||||
required String id,
|
||||
required int position,
|
||||
required String name,
|
||||
required ExerciseStepType type,
|
||||
required int defaultTargetValue,
|
||||
bool hasScore = false,
|
||||
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
||||
String? scoreLabel,
|
||||
String? scoreUnit,
|
||||
double? defaultTargetScore,
|
||||
int? defaultTargetScoreTimeMs,
|
||||
}) {
|
||||
return ExerciseStep(
|
||||
id: id,
|
||||
position: position,
|
||||
name: name,
|
||||
type: type,
|
||||
defaultTargetValue: defaultTargetValue,
|
||||
hasScore: hasScore,
|
||||
scoreInputMode: scoreInputMode,
|
||||
scoreLabel: scoreLabel,
|
||||
scoreUnit: scoreUnit,
|
||||
defaultTargetScore: defaultTargetScore,
|
||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||
);
|
||||
}
|
||||
|
||||
void _expectExerciseEquals(Exercise actual, Exercise expected) {
|
||||
_expectMetadataEquals(actual.metadata, expected.metadata);
|
||||
expect(actual.name, expected.name);
|
||||
expect(actual.description, expected.description);
|
||||
expect(actual.imageMediaIds, expected.imageMediaIds);
|
||||
expect(actual.iconMediaId, expected.iconMediaId);
|
||||
expect(actual.videoMediaId, expected.videoMediaId);
|
||||
expect(actual.hasTimeMeasure, expected.hasTimeMeasure);
|
||||
expect(actual.hasRepsMeasure, expected.hasRepsMeasure);
|
||||
expect(actual.hasScoreMeasure, expected.hasScoreMeasure);
|
||||
expect(actual.scoreInputMode, expected.scoreInputMode);
|
||||
expect(actual.scoreLabel, expected.scoreLabel);
|
||||
expect(actual.scoreUnit, expected.scoreUnit);
|
||||
expect(actual.defaultTargetTimeSeconds, expected.defaultTargetTimeSeconds);
|
||||
expect(actual.defaultTargetReps, expected.defaultTargetReps);
|
||||
expect(actual.defaultTargetScore, expected.defaultTargetScore);
|
||||
expect(actual.defaultTargetScoreTimeMs, expected.defaultTargetScoreTimeMs);
|
||||
expect(actual.autoStartNextTimedStep, expected.autoStartNextTimedStep);
|
||||
expect(actual.archivedAt?.toUtc(), expected.archivedAt?.toUtc());
|
||||
expect(actual.steps, hasLength(expected.steps.length));
|
||||
for (var index = 0; index < expected.steps.length; index++) {
|
||||
_expectExerciseStepEquals(actual.steps[index], expected.steps[index]);
|
||||
}
|
||||
}
|
||||
|
||||
void _expectExerciseStepEquals(ExerciseStep actual, ExerciseStep expected) {
|
||||
expect(actual.id, expected.id);
|
||||
expect(actual.position, expected.position);
|
||||
expect(actual.name, expected.name);
|
||||
expect(actual.type, expected.type);
|
||||
expect(actual.defaultTargetValue, expected.defaultTargetValue);
|
||||
expect(actual.hasScore, expected.hasScore);
|
||||
expect(actual.scoreInputMode, expected.scoreInputMode);
|
||||
expect(actual.scoreLabel, expected.scoreLabel);
|
||||
expect(actual.scoreUnit, expected.scoreUnit);
|
||||
expect(actual.defaultTargetScore, expected.defaultTargetScore);
|
||||
expect(actual.defaultTargetScoreTimeMs, expected.defaultTargetScoreTimeMs);
|
||||
}
|
||||
|
||||
void _expectMetadataEquals(EntityMetadata actual, EntityMetadata expected) {
|
||||
expect(actual.id, expected.id);
|
||||
expect(actual.createdAt.toUtc(), expected.createdAt.toUtc());
|
||||
expect(actual.updatedAt.toUtc(), expected.updatedAt.toUtc());
|
||||
expect(actual.deletedAt?.toUtc(), expected.deletedAt?.toUtc());
|
||||
expect(actual.schemaVersion, expected.schemaVersion);
|
||||
expect(actual.syncState, expected.syncState);
|
||||
expect(actual.localRevision, expected.localRevision);
|
||||
expect(actual.originDeviceId, expected.originDeviceId);
|
||||
expect(actual.futureOwnerProfileId, expected.futureOwnerProfileId);
|
||||
expect(actual.lastSyncedAt?.toUtc(), expected.lastSyncedAt?.toUtc());
|
||||
expect(actual.remoteRevision, expected.remoteRevision);
|
||||
}
|
||||
|
||||
final class _ExerciseRoundTripCase {
|
||||
const _ExerciseRoundTripCase({required this.label, required this.exercise});
|
||||
|
||||
final String label;
|
||||
final Exercise exercise;
|
||||
}
|
||||
|
||||
final class _FakeClock implements Clock {
|
||||
const _FakeClock(this.value);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user