feat(ui): réglages auto-enchaînement exercice, programme et séance (ticket #77)

Ajoute les réglages d'auto-enchaînement des chronos d'étapes sur
exercise_library_screen.dart, program_screen.dart et
workout_template_screen.dart, s'appuyant sur la résolution du ticket
#76. Propagation du nouveau champ vérifiée exhaustivement sur les 5
points d'impact de _ProgramExerciseDraft (constructeur, fromExercise,
fromProgramExercise, copy(), toConfig()) — pas de répétition du bug
#72. flutter pub get OK, dart format appliqué (7 fichiers reformatés),
analyze propre (mêmes infos préexistantes), 152/152 tests verts, build
APK debug validé. Développés dans le même worktree partagé par
DevBackend et DevFrontend ; feature/step-chaining-77-ui-settings
restée vide, jamais divergée.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 13:57:01 +02:00
parent ea1e240c87
commit 4b355152d5
6 changed files with 505 additions and 1 deletions

View File

@ -404,6 +404,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
var _importingImage = false; var _importingImage = false;
var _importingVideo = false; var _importingVideo = false;
var _stepsEnabled = false; var _stepsEnabled = false;
var _autoStartNextTimedStep = true;
var _nextStepDraftId = 0; var _nextStepDraftId = 0;
final _stepDrafts = <_ExerciseStepDraft>[]; final _stepDrafts = <_ExerciseStepDraft>[];
String? _measureError; String? _measureError;
@ -442,6 +443,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
_hasScore = exercise?.hasScoreMeasure ?? false; _hasScore = exercise?.hasScoreMeasure ?? false;
_scoreInputMode = exercise?.scoreInputMode ?? ScoreInputMode.manual; _scoreInputMode = exercise?.scoreInputMode ?? ScoreInputMode.manual;
_stepsEnabled = exercise?.steps.isNotEmpty ?? false; _stepsEnabled = exercise?.steps.isNotEmpty ?? false;
_autoStartNextTimedStep = exercise?.autoStartNextTimedStep ?? true;
for (final step in exercise?.steps ?? const <ExerciseStep>[]) { for (final step in exercise?.steps ?? const <ExerciseStep>[]) {
_stepDrafts.add(_draftFromStep(step)); _stepDrafts.add(_draftFromStep(step));
} }
@ -742,6 +744,25 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
}, },
), ),
if (_stepsEnabled) ...[ if (_stepsEnabled) ...[
const SizedBox(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text(
'Enchaîner automatiquement les chronos consécutifs',
),
subtitle: Text(
_autoStartNextTimedStep
? 'Le chrono suivant démarre dès que le précédent arrive à 0.'
: "L'app attendra ton démarrage avant de lancer le chrono "
'suivant.',
),
value: _autoStartNextTimedStep,
onChanged: (value) {
setState(() {
_autoStartNextTimedStep = value;
});
},
),
const SizedBox(height: 8), const SizedBox(height: 8),
ReorderableListView.builder( ReorderableListView.builder(
shrinkWrap: true, shrinkWrap: true,
@ -1312,6 +1333,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
defaultTargetReps: defaultTargetReps, defaultTargetReps: defaultTargetReps,
defaultTargetScore: defaultTargetScore, defaultTargetScore: defaultTargetScore,
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs, defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
autoStartNextTimedStep: _autoStartNextTimedStep,
steps: steps, steps: steps,
); );
} else { } else {
@ -1332,6 +1354,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
defaultTargetReps: defaultTargetReps, defaultTargetReps: defaultTargetReps,
defaultTargetScore: defaultTargetScore, defaultTargetScore: defaultTargetScore,
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs, defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
autoStartNextTimedStep: _autoStartNextTimedStep,
steps: steps, steps: steps,
); );
} }

View File

@ -660,6 +660,11 @@ final class _ProgramExerciseCustomizationScreenState
: scoreUnit == null : scoreUnit == null
? 'Score' ? 'Score'
: 'Score ($scoreUnit)'; : 'Score ($scoreUnit)';
final autoStartNextTimedStepCustomized =
_draft.autoStartNextTimedStepOverride != null;
final autoStartNextTimedStepStateLabel = autoStartNextTimedStepCustomized
? 'Personnalisé pour ce programme'
: "Réglage de l'exercice";
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text("Personnaliser l'exercice")), appBar: AppBar(title: const Text("Personnaliser l'exercice")),
bottomNavigationBar: SafeArea( bottomNavigationBar: SafeArea(
@ -785,6 +790,36 @@ final class _ProgramExerciseCustomizationScreenState
_draft.targetScoreTimeMs = null; _draft.targetScoreTimeMs = null;
}, },
), ),
if (_draft.exerciseStepsSnapshot.isNotEmpty) ...[
const SizedBox(height: 24),
Text('Séquence', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text(
'Enchaîner automatiquement les chronos consécutifs',
),
subtitle: Text(
'$autoStartNextTimedStepStateLabel\n'
'${_autoStartHelpText(_draft.autoStartNextTimedStepEffective)}',
),
value: _draft.autoStartNextTimedStepEffective,
onChanged: (value) {
setState(() {
_draft.autoStartNextTimedStepOverride = value;
});
},
),
if (autoStartNextTimedStepCustomized)
TextButton(
onPressed: () {
setState(() {
_draft.autoStartNextTimedStepOverride = null;
});
},
child: const Text("Revenir au réglage de l'exercice"),
),
],
const SizedBox(height: 24), const SizedBox(height: 24),
Text('Repos', style: Theme.of(context).textTheme.titleMedium), Text('Repos', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -916,6 +951,8 @@ final class _ProgramExerciseDraft {
this.exerciseImageMediaIdsSnapshot = const [], this.exerciseImageMediaIdsSnapshot = const [],
this.exerciseVideoMediaIdSnapshot, this.exerciseVideoMediaIdSnapshot,
this.exerciseStepsSnapshot = const [], this.exerciseStepsSnapshot = const [],
this.autoStartNextTimedStepSnapshot = true,
this.autoStartNextTimedStepOverride,
this.exerciseArchivedSnapshot = false, this.exerciseArchivedSnapshot = false,
required this.availableTimeSnapshot, required this.availableTimeSnapshot,
required this.availableRepsSnapshot, required this.availableRepsSnapshot,
@ -946,6 +983,7 @@ final class _ProgramExerciseDraft {
exerciseImageMediaIdsSnapshot: exercise.imageMediaIds, exerciseImageMediaIdsSnapshot: exercise.imageMediaIds,
exerciseVideoMediaIdSnapshot: exercise.videoMediaId, exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
exerciseStepsSnapshot: exercise.steps, exerciseStepsSnapshot: exercise.steps,
autoStartNextTimedStepSnapshot: exercise.autoStartNextTimedStep,
exerciseArchivedSnapshot: exercise.archivedAt != null, exerciseArchivedSnapshot: exercise.archivedAt != null,
availableTimeSnapshot: exercise.hasTimeMeasure, availableTimeSnapshot: exercise.hasTimeMeasure,
availableRepsSnapshot: exercise.hasRepsMeasure, availableRepsSnapshot: exercise.hasRepsMeasure,
@ -978,6 +1016,8 @@ final class _ProgramExerciseDraft {
exerciseImageMediaIdsSnapshot: exercise.exerciseImageMediaIdsSnapshot, exerciseImageMediaIdsSnapshot: exercise.exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exercise.exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot: exercise.exerciseVideoMediaIdSnapshot,
exerciseStepsSnapshot: exercise.exerciseStepsSnapshot, exerciseStepsSnapshot: exercise.exerciseStepsSnapshot,
autoStartNextTimedStepSnapshot: exercise.autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride: exercise.autoStartNextTimedStepOverride,
exerciseArchivedSnapshot: exercise.exerciseArchivedSnapshot, exerciseArchivedSnapshot: exercise.exerciseArchivedSnapshot,
availableTimeSnapshot: exercise.availableTimeSnapshot, availableTimeSnapshot: exercise.availableTimeSnapshot,
availableRepsSnapshot: exercise.availableRepsSnapshot, availableRepsSnapshot: exercise.availableRepsSnapshot,
@ -1008,6 +1048,7 @@ final class _ProgramExerciseDraft {
final List<String> exerciseImageMediaIdsSnapshot; final List<String> exerciseImageMediaIdsSnapshot;
final String? exerciseVideoMediaIdSnapshot; final String? exerciseVideoMediaIdSnapshot;
final List<ExerciseStep> exerciseStepsSnapshot; final List<ExerciseStep> exerciseStepsSnapshot;
final bool autoStartNextTimedStepSnapshot;
final bool exerciseArchivedSnapshot; final bool exerciseArchivedSnapshot;
final bool availableTimeSnapshot; final bool availableTimeSnapshot;
final bool availableRepsSnapshot; final bool availableRepsSnapshot;
@ -1022,6 +1063,11 @@ final class _ProgramExerciseDraft {
double? targetScore; double? targetScore;
int? targetScoreTimeMs; int? targetScoreTimeMs;
int? restSecondsOverride; int? restSecondsOverride;
bool? autoStartNextTimedStepOverride;
bool get autoStartNextTimedStepEffective {
return autoStartNextTimedStepOverride ?? autoStartNextTimedStepSnapshot;
}
_ProgramExerciseDraft copy() { _ProgramExerciseDraft copy() {
return _ProgramExerciseDraft( return _ProgramExerciseDraft(
@ -1034,6 +1080,8 @@ final class _ProgramExerciseDraft {
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot, exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
exerciseStepsSnapshot: exerciseStepsSnapshot, exerciseStepsSnapshot: exerciseStepsSnapshot,
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride: autoStartNextTimedStepOverride,
exerciseArchivedSnapshot: exerciseArchivedSnapshot, exerciseArchivedSnapshot: exerciseArchivedSnapshot,
availableTimeSnapshot: availableTimeSnapshot, availableTimeSnapshot: availableTimeSnapshot,
availableRepsSnapshot: availableRepsSnapshot, availableRepsSnapshot: availableRepsSnapshot,
@ -1063,6 +1111,8 @@ final class _ProgramExerciseDraft {
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot, exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot, exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
exerciseStepsSnapshot: exerciseStepsSnapshot, exerciseStepsSnapshot: exerciseStepsSnapshot,
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
autoStartNextTimedStepOverride: autoStartNextTimedStepOverride,
exerciseArchivedSnapshot: exerciseArchivedSnapshot, exerciseArchivedSnapshot: exerciseArchivedSnapshot,
availableTimeSnapshot: availableTimeSnapshot, availableTimeSnapshot: availableTimeSnapshot,
availableRepsSnapshot: availableRepsSnapshot, availableRepsSnapshot: availableRepsSnapshot,
@ -1083,6 +1133,12 @@ final class _ProgramExerciseDraft {
} }
} }
String _autoStartHelpText(bool active) {
return active
? 'Le chrono suivant démarre dès que le précédent arrive à 0.'
: "L'app attendra ton démarrage avant de lancer le chrono suivant.";
}
double? _millisecondsToSeconds(int? milliseconds) { double? _millisecondsToSeconds(int? milliseconds) {
return milliseconds == null ? null : milliseconds / 1000; return milliseconds == null ? null : milliseconds / 1000;
} }

View File

@ -502,6 +502,12 @@ final class _WorkoutTemplateProgramDetailScreenState
itemCount: widget.program.exercises.length, itemCount: widget.program.exercises.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final exercise = widget.program.exercises[index]; final exercise = widget.program.exercises[index];
final autoStartNextTimedStepCustomized =
exercise.autoStartNextTimedStepOverride != null;
final autoStartNextTimedStepStateLabel =
autoStartNextTimedStepCustomized
? 'Personnalisé pour cette séance'
: 'Réglage du programme';
return Card( return Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
@ -564,6 +570,38 @@ final class _WorkoutTemplateProgramDetailScreenState
}); });
}, },
), ),
if (exercise.exerciseStepsSnapshot.isNotEmpty) ...[
const SizedBox(height: 16),
Text(
'Séquence',
style: Theme.of(context).textTheme.titleMedium,
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text(
'Enchaîner automatiquement les chronos consécutifs',
),
subtitle: Text(
'$autoStartNextTimedStepStateLabel\n'
'${_autoStartHelpText(exercise.autoStartNextTimedStepEffective)}',
),
value: exercise.autoStartNextTimedStepEffective,
onChanged: (value) {
setState(() {
exercise.autoStartNextTimedStepOverride = value;
});
},
),
if (autoStartNextTimedStepCustomized)
TextButton(
onPressed: () {
setState(() {
exercise.autoStartNextTimedStepOverride = null;
});
},
child: const Text('Revenir au réglage du programme'),
),
],
], ],
), ),
), ),
@ -621,6 +659,8 @@ final class WorkoutTemplateProgramDraft {
override.targetTimeSecondsOverride; override.targetTimeSecondsOverride;
exercise.targetRepsOverride = override.targetRepsOverride; exercise.targetRepsOverride = override.targetRepsOverride;
exercise.targetScoreOverride = override.targetScoreOverride; exercise.targetScoreOverride = override.targetScoreOverride;
exercise.autoStartNextTimedStepOverride =
override.autoStartNextTimedStepOverride;
} }
} }
} }
@ -667,6 +707,7 @@ final class WorkoutTemplateProgramDraft {
targetTimeSecondsOverride: exercise.targetTimeSecondsOverride, targetTimeSecondsOverride: exercise.targetTimeSecondsOverride,
targetRepsOverride: exercise.targetRepsOverride, targetRepsOverride: exercise.targetRepsOverride,
targetScoreOverride: exercise.targetScoreOverride, targetScoreOverride: exercise.targetScoreOverride,
autoStartNextTimedStepOverride: exercise.autoStartNextTimedStepOverride,
); );
} }
} }
@ -680,6 +721,8 @@ final class WorkoutTemplateExerciseDraft {
required this.timeEnabled, required this.timeEnabled,
required this.repsEnabled, required this.repsEnabled,
required this.scoreEnabled, required this.scoreEnabled,
this.exerciseStepsSnapshot = const [],
this.autoStartNextTimedStepSnapshot = true,
this.scoreUnitSnapshot, this.scoreUnitSnapshot,
this.targetTimeSeconds, this.targetTimeSeconds,
this.targetReps, this.targetReps,
@ -689,6 +732,7 @@ final class WorkoutTemplateExerciseDraft {
this.targetTimeSecondsOverride, this.targetTimeSecondsOverride,
this.targetRepsOverride, this.targetRepsOverride,
this.targetScoreOverride, this.targetScoreOverride,
this.autoStartNextTimedStepOverride,
}); });
final String snapshotExerciseId; final String snapshotExerciseId;
@ -697,6 +741,8 @@ final class WorkoutTemplateExerciseDraft {
final bool timeEnabled; final bool timeEnabled;
final bool repsEnabled; final bool repsEnabled;
final bool scoreEnabled; final bool scoreEnabled;
final List<ExerciseStep> exerciseStepsSnapshot;
final bool autoStartNextTimedStepSnapshot;
final String? scoreUnitSnapshot; final String? scoreUnitSnapshot;
final int? targetTimeSeconds; final int? targetTimeSeconds;
final int? targetReps; final int? targetReps;
@ -706,12 +752,18 @@ final class WorkoutTemplateExerciseDraft {
int? targetTimeSecondsOverride; int? targetTimeSecondsOverride;
int? targetRepsOverride; int? targetRepsOverride;
double? targetScoreOverride; double? targetScoreOverride;
bool? autoStartNextTimedStepOverride;
bool get autoStartNextTimedStepEffective {
return autoStartNextTimedStepOverride ?? autoStartNextTimedStepSnapshot;
}
bool get hasOverride { bool get hasOverride {
return setsCountOverride != null || return setsCountOverride != null ||
targetTimeSecondsOverride != null || targetTimeSecondsOverride != null ||
targetRepsOverride != null || targetRepsOverride != null ||
targetScoreOverride != null; targetScoreOverride != null ||
autoStartNextTimedStepOverride != null;
} }
} }
@ -746,6 +798,10 @@ List<WorkoutTemplateExerciseDraft> _parseExercises(
final rawExercises = snapshot['exercises'] as List<dynamic>? ?? const []; final rawExercises = snapshot['exercises'] as List<dynamic>? ?? const [];
return rawExercises.map((raw) { return rawExercises.map((raw) {
final exercise = raw as Map<String, dynamic>; final exercise = raw as Map<String, dynamic>;
final autoStartNextTimedStepSnapshot =
(exercise['autoStartNextTimedStepOverride'] as bool?) ??
(exercise['autoStartNextTimedStepSnapshot'] as bool?) ??
true;
return WorkoutTemplateExerciseDraft( return WorkoutTemplateExerciseDraft(
snapshotExerciseId: exercise['id'] as String, snapshotExerciseId: exercise['id'] as String,
exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String, exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String,
@ -753,6 +809,10 @@ List<WorkoutTemplateExerciseDraft> _parseExercises(
timeEnabled: exercise['timeEnabled'] as bool, timeEnabled: exercise['timeEnabled'] as bool,
repsEnabled: exercise['repsEnabled'] as bool, repsEnabled: exercise['repsEnabled'] as bool,
scoreEnabled: exercise['scoreEnabled'] as bool, scoreEnabled: exercise['scoreEnabled'] as bool,
exerciseStepsSnapshot: _parseExerciseSteps(
exercise['exerciseStepsSnapshot'],
),
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?, scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
targetTimeSeconds: exercise['targetTimeSeconds'] as int?, targetTimeSeconds: exercise['targetTimeSeconds'] as int?,
targetReps: exercise['targetReps'] as int?, targetReps: exercise['targetReps'] as int?,
@ -761,6 +821,34 @@ List<WorkoutTemplateExerciseDraft> _parseExercises(
}).toList(); }).toList();
} }
List<ExerciseStep> _parseExerciseSteps(Object? value) {
final rawSteps = value as List<dynamic>? ?? const [];
return rawSteps.map((raw) {
final step = raw as Map<String, dynamic>;
return ExerciseStep(
id: step['id'] as String,
position: step['position'] as int,
name: step['name'] as String,
type: ExerciseStepType.values.byName(step['type'] as String),
defaultTargetValue: step['defaultTargetValue'] as int,
hasScore: step['hasScore'] as bool? ?? false,
scoreInputMode: ScoreInputMode.values.byName(
step['scoreInputMode'] as String? ?? ScoreInputMode.manual.name,
),
scoreLabel: step['scoreLabel'] as String?,
scoreUnit: step['scoreUnit'] as String?,
defaultTargetScore: (step['defaultTargetScore'] as num?)?.toDouble(),
defaultTargetScoreTimeMs: step['defaultTargetScoreTimeMs'] as int?,
);
}).toList();
}
String _autoStartHelpText(bool active) {
return active
? 'Le chrono suivant démarre dès que le précédent arrive à 0.'
: "L'app attendra ton démarrage avant de lancer le chrono suivant.";
}
String _templateSummary(WorkoutTemplate template) { String _templateSummary(WorkoutTemplate template) {
final programCount = template.programs.length; final programCount = template.programs.length;
final exerciseCount = template.programs.fold<int>( final exerciseCount = template.programs.fold<int>(

View File

@ -435,6 +435,58 @@ void main() {
expect(saved.steps.single.defaultTargetValue, 12); expect(saved.steps.single.defaultTargetValue, 12);
}); });
testWidgets(
'le réglage denchaînement automatique des étapes est sauvegardé',
(tester) async {
final exerciseRepository = _FakeExerciseRepository();
await tester.binding.setSurfaceSize(const Size(400, 3600));
addTearDown(() => tester.binding.setSurfaceSize(null));
await _pumpExerciseForm(tester, exerciseRepository);
await tester.enterText(
find.widgetWithText(TextFormField, 'Nom'),
'Combo',
);
await tester.enterText(
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
'30',
);
await tester.tap(
find.widgetWithText(
SwitchListTile,
'Rythmer cet exercice avec des étapes',
),
);
await tester.pump();
await tester.tap(
find.widgetWithText(
SwitchListTile,
'Enchaîner automatiquement les chronos consécutifs',
),
);
await tester.pump();
await tester.tap(find.text('Ajouter une étape'));
await tester.pump();
await tester.enterText(
find.widgetWithText(TextFormField, 'Nom de létape').last,
'Dribble main droite',
);
await tester.enterText(
find.widgetWithText(TextFormField, 'Durée par défaut (s)').last,
'12',
);
await tester.ensureVisible(find.text('Enregistrer'));
await tester.tap(find.text('Enregistrer'));
await tester.pumpAndSettle();
final saved = exerciseRepository.saved.single;
expect(saved.steps, hasLength(1));
expect(saved.autoStartNextTimedStep, isFalse);
},
);
testWidgets('désigner une image comme icône lindique visuellement', ( testWidgets('désigner une image comme icône lindique visuellement', (
tester, tester,
) async { ) async {

View File

@ -306,6 +306,165 @@ void main() {
}, },
); );
testWidgets(
'personnaliser lenchaînement automatique transmet loverride au programme',
(tester) async {
final steps = [
_step(
id: 'step-1',
position: 0,
name: 'Course',
type: ExerciseStepType.time,
),
_step(
id: 'step-2',
position: 1,
name: 'Retour',
type: ExerciseStepType.time,
),
];
final programRepository = _FakeProgramRepository();
final exerciseRepository = _FakeExerciseRepository()
..exercises.add(
Exercise(
metadata: _metadata('exercise-steps'),
name: 'Navettes',
hasTimeMeasure: true,
hasRepsMeasure: false,
hasScoreMeasure: false,
defaultTargetTimeSeconds: 30,
steps: steps,
),
);
await tester.binding.setSurfaceSize(const Size(400, 2400));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
programRepository,
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
),
),
);
await tester.enterText(
find.widgetWithText(TextFormField, 'Nom'),
'Programme étapes',
);
await tester.tap(find.text('Ajouter un exercice'));
await tester.pumpAndSettle();
await tester.tap(find.text('Navettes'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip("Personnaliser l'exercice"));
await tester.pumpAndSettle();
await tester.tap(
find.widgetWithText(
SwitchListTile,
'Enchaîner automatiquement les chronos consécutifs',
),
);
await tester.pump();
expect(
find.textContaining('Personnalisé pour ce programme'),
findsOneWidget,
);
await tester.tap(find.text('Enregistrer'));
await tester.pumpAndSettle();
await tester.tap(find.text('Enregistrer'));
await tester.pumpAndSettle();
final savedExercise = programRepository.programs.single.exercises.single;
expect(savedExercise.autoStartNextTimedStepSnapshot, isTrue);
expect(savedExercise.autoStartNextTimedStepOverride, isFalse);
},
);
testWidgets(
'revenir au réglage de lexercice remet loverride programme à null',
(tester) async {
final steps = [
_step(
id: 'step-1',
position: 0,
name: 'Course',
type: ExerciseStepType.time,
),
];
final programRepository = _FakeProgramRepository();
final exerciseRepository = _FakeExerciseRepository()
..exercises.add(
Exercise(
metadata: _metadata('exercise-steps'),
name: 'Navettes',
hasTimeMeasure: true,
hasRepsMeasure: false,
hasScoreMeasure: false,
defaultTargetTimeSeconds: 30,
autoStartNextTimedStep: false,
steps: steps,
),
);
await tester.binding.setSurfaceSize(const Size(400, 2400));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
programRepository,
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
),
),
);
await tester.enterText(
find.widgetWithText(TextFormField, 'Nom'),
'Programme étapes',
);
await tester.tap(find.text('Ajouter un exercice'));
await tester.pumpAndSettle();
await tester.tap(find.text('Navettes'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip("Personnaliser l'exercice"));
await tester.pumpAndSettle();
await tester.tap(
find.widgetWithText(
SwitchListTile,
'Enchaîner automatiquement les chronos consécutifs',
),
);
await tester.pump();
expect(
find.textContaining('Personnalisé pour ce programme'),
findsOneWidget,
);
await tester.tap(find.text("Revenir au réglage de l'exercice"));
await tester.pump();
expect(find.textContaining("Réglage de l'exercice"), findsOneWidget);
await tester.tap(find.text('Enregistrer'));
await tester.pumpAndSettle();
await tester.tap(find.text('Enregistrer'));
await tester.pumpAndSettle();
final savedExercise = programRepository.programs.single.exercises.single;
expect(savedExercise.autoStartNextTimedStepSnapshot, isFalse);
expect(savedExercise.autoStartNextTimedStepOverride, isNull);
},
);
testWidgets('affiche lobjectif de chrono pour un score en mode chrono', ( testWidgets('affiche lobjectif de chrono pour un score en mode chrono', (
tester, tester,
) async { ) async {

View File

@ -44,6 +44,117 @@ void main() {
}, },
); );
testWidgets(
'personnaliser lenchaînement automatique transmet loverride séance',
(tester) async {
final program = WorkoutTemplateProgramDraft(
clientKey: 'program-1',
programNameSnapshot: 'Circuit',
defaultRestSecondsSnapshot: 60,
programSnapshotJson: '{}',
exercises: [
WorkoutTemplateExerciseDraft(
snapshotExerciseId: 'exercise-1',
exerciseNameSnapshot: 'Navettes',
setsCount: 3,
timeEnabled: true,
repsEnabled: false,
scoreEnabled: false,
exerciseStepsSnapshot: [
_step(
id: 'step-1',
position: 0,
name: 'Course',
type: ExerciseStepType.time,
),
_step(
id: 'step-2',
position: 1,
name: 'Retour',
type: ExerciseStepType.time,
),
],
autoStartNextTimedStepSnapshot: true,
targetTimeSeconds: 30,
),
],
);
await tester.binding.setSurfaceSize(const Size(400, 1400));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
MaterialApp(home: WorkoutTemplateProgramDetailScreen(program: program)),
);
await tester.tap(
find.widgetWithText(
SwitchListTile,
'Enchaîner automatiquement les chronos consécutifs',
),
);
await tester.pump();
expect(
find.textContaining('Personnalisé pour cette séance'),
findsOneWidget,
);
final override = program.toOverrides().single;
expect(override.autoStartNextTimedStepOverride, isFalse);
},
);
testWidgets(
'revenir au réglage du programme remet loverride séance à null',
(tester) async {
final program = WorkoutTemplateProgramDraft(
clientKey: 'program-1',
programNameSnapshot: 'Circuit',
defaultRestSecondsSnapshot: 60,
programSnapshotJson: '{}',
exercises: [
WorkoutTemplateExerciseDraft(
snapshotExerciseId: 'exercise-1',
exerciseNameSnapshot: 'Navettes',
setsCount: 3,
timeEnabled: true,
repsEnabled: false,
scoreEnabled: false,
exerciseStepsSnapshot: [
_step(
id: 'step-1',
position: 0,
name: 'Course',
type: ExerciseStepType.time,
),
],
autoStartNextTimedStepSnapshot: false,
autoStartNextTimedStepOverride: true,
targetTimeSeconds: 30,
),
],
);
await tester.binding.setSurfaceSize(const Size(400, 1400));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
MaterialApp(home: WorkoutTemplateProgramDetailScreen(program: program)),
);
expect(
find.textContaining('Personnalisé pour cette séance'),
findsOneWidget,
);
await tester.tap(find.text('Revenir au réglage du programme'));
await tester.pump();
expect(find.textContaining('Réglage du programme'), findsOneWidget);
expect(program.toOverrides(), isEmpty);
},
);
testWidgets( testWidgets(
'lancer une séance alors quune autre est en cours affiche un message', 'lancer une séance alors quune autre est en cours affiche un message',
(tester) async { (tester) async {
@ -308,6 +419,21 @@ EntityMetadata _metadata(String id) {
); );
} }
ExerciseStep _step({
required String id,
required int position,
required String name,
required ExerciseStepType type,
}) {
return ExerciseStep(
id: id,
position: position,
name: name,
type: type,
defaultTargetValue: type == ExerciseStepType.time ? 30 : 5,
);
}
final class _FakeClock implements Clock { final class _FakeClock implements Clock {
const _FakeClock(this.value); const _FakeClock(this.value);