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:
@ -404,6 +404,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
var _importingImage = false;
|
||||
var _importingVideo = false;
|
||||
var _stepsEnabled = false;
|
||||
var _autoStartNextTimedStep = true;
|
||||
var _nextStepDraftId = 0;
|
||||
final _stepDrafts = <_ExerciseStepDraft>[];
|
||||
String? _measureError;
|
||||
@ -442,6 +443,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
_hasScore = exercise?.hasScoreMeasure ?? false;
|
||||
_scoreInputMode = exercise?.scoreInputMode ?? ScoreInputMode.manual;
|
||||
_stepsEnabled = exercise?.steps.isNotEmpty ?? false;
|
||||
_autoStartNextTimedStep = exercise?.autoStartNextTimedStep ?? true;
|
||||
for (final step in exercise?.steps ?? const <ExerciseStep>[]) {
|
||||
_stepDrafts.add(_draftFromStep(step));
|
||||
}
|
||||
@ -742,6 +744,25 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
},
|
||||
),
|
||||
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),
|
||||
ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
@ -1312,6 +1333,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
defaultTargetReps: defaultTargetReps,
|
||||
defaultTargetScore: defaultTargetScore,
|
||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||
autoStartNextTimedStep: _autoStartNextTimedStep,
|
||||
steps: steps,
|
||||
);
|
||||
} else {
|
||||
@ -1332,6 +1354,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
defaultTargetReps: defaultTargetReps,
|
||||
defaultTargetScore: defaultTargetScore,
|
||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||
autoStartNextTimedStep: _autoStartNextTimedStep,
|
||||
steps: steps,
|
||||
);
|
||||
}
|
||||
|
||||
@ -660,6 +660,11 @@ final class _ProgramExerciseCustomizationScreenState
|
||||
: scoreUnit == null
|
||||
? 'Score'
|
||||
: 'Score ($scoreUnit)';
|
||||
final autoStartNextTimedStepCustomized =
|
||||
_draft.autoStartNextTimedStepOverride != null;
|
||||
final autoStartNextTimedStepStateLabel = autoStartNextTimedStepCustomized
|
||||
? 'Personnalisé pour ce programme'
|
||||
: "Réglage de l'exercice";
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Personnaliser l'exercice")),
|
||||
bottomNavigationBar: SafeArea(
|
||||
@ -785,6 +790,36 @@ final class _ProgramExerciseCustomizationScreenState
|
||||
_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),
|
||||
Text('Repos', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
@ -916,6 +951,8 @@ final class _ProgramExerciseDraft {
|
||||
this.exerciseImageMediaIdsSnapshot = const [],
|
||||
this.exerciseVideoMediaIdSnapshot,
|
||||
this.exerciseStepsSnapshot = const [],
|
||||
this.autoStartNextTimedStepSnapshot = true,
|
||||
this.autoStartNextTimedStepOverride,
|
||||
this.exerciseArchivedSnapshot = false,
|
||||
required this.availableTimeSnapshot,
|
||||
required this.availableRepsSnapshot,
|
||||
@ -946,6 +983,7 @@ final class _ProgramExerciseDraft {
|
||||
exerciseImageMediaIdsSnapshot: exercise.imageMediaIds,
|
||||
exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
|
||||
exerciseStepsSnapshot: exercise.steps,
|
||||
autoStartNextTimedStepSnapshot: exercise.autoStartNextTimedStep,
|
||||
exerciseArchivedSnapshot: exercise.archivedAt != null,
|
||||
availableTimeSnapshot: exercise.hasTimeMeasure,
|
||||
availableRepsSnapshot: exercise.hasRepsMeasure,
|
||||
@ -978,6 +1016,8 @@ final class _ProgramExerciseDraft {
|
||||
exerciseImageMediaIdsSnapshot: exercise.exerciseImageMediaIdsSnapshot,
|
||||
exerciseVideoMediaIdSnapshot: exercise.exerciseVideoMediaIdSnapshot,
|
||||
exerciseStepsSnapshot: exercise.exerciseStepsSnapshot,
|
||||
autoStartNextTimedStepSnapshot: exercise.autoStartNextTimedStepSnapshot,
|
||||
autoStartNextTimedStepOverride: exercise.autoStartNextTimedStepOverride,
|
||||
exerciseArchivedSnapshot: exercise.exerciseArchivedSnapshot,
|
||||
availableTimeSnapshot: exercise.availableTimeSnapshot,
|
||||
availableRepsSnapshot: exercise.availableRepsSnapshot,
|
||||
@ -1008,6 +1048,7 @@ final class _ProgramExerciseDraft {
|
||||
final List<String> exerciseImageMediaIdsSnapshot;
|
||||
final String? exerciseVideoMediaIdSnapshot;
|
||||
final List<ExerciseStep> exerciseStepsSnapshot;
|
||||
final bool autoStartNextTimedStepSnapshot;
|
||||
final bool exerciseArchivedSnapshot;
|
||||
final bool availableTimeSnapshot;
|
||||
final bool availableRepsSnapshot;
|
||||
@ -1022,6 +1063,11 @@ final class _ProgramExerciseDraft {
|
||||
double? targetScore;
|
||||
int? targetScoreTimeMs;
|
||||
int? restSecondsOverride;
|
||||
bool? autoStartNextTimedStepOverride;
|
||||
|
||||
bool get autoStartNextTimedStepEffective {
|
||||
return autoStartNextTimedStepOverride ?? autoStartNextTimedStepSnapshot;
|
||||
}
|
||||
|
||||
_ProgramExerciseDraft copy() {
|
||||
return _ProgramExerciseDraft(
|
||||
@ -1034,6 +1080,8 @@ final class _ProgramExerciseDraft {
|
||||
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
|
||||
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
|
||||
exerciseStepsSnapshot: exerciseStepsSnapshot,
|
||||
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
|
||||
autoStartNextTimedStepOverride: autoStartNextTimedStepOverride,
|
||||
exerciseArchivedSnapshot: exerciseArchivedSnapshot,
|
||||
availableTimeSnapshot: availableTimeSnapshot,
|
||||
availableRepsSnapshot: availableRepsSnapshot,
|
||||
@ -1063,6 +1111,8 @@ final class _ProgramExerciseDraft {
|
||||
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
|
||||
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
|
||||
exerciseStepsSnapshot: exerciseStepsSnapshot,
|
||||
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
|
||||
autoStartNextTimedStepOverride: autoStartNextTimedStepOverride,
|
||||
exerciseArchivedSnapshot: exerciseArchivedSnapshot,
|
||||
availableTimeSnapshot: availableTimeSnapshot,
|
||||
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) {
|
||||
return milliseconds == null ? null : milliseconds / 1000;
|
||||
}
|
||||
|
||||
@ -502,6 +502,12 @@ final class _WorkoutTemplateProgramDetailScreenState
|
||||
itemCount: widget.program.exercises.length,
|
||||
itemBuilder: (context, 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(
|
||||
child: Padding(
|
||||
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;
|
||||
exercise.targetRepsOverride = override.targetRepsOverride;
|
||||
exercise.targetScoreOverride = override.targetScoreOverride;
|
||||
exercise.autoStartNextTimedStepOverride =
|
||||
override.autoStartNextTimedStepOverride;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -667,6 +707,7 @@ final class WorkoutTemplateProgramDraft {
|
||||
targetTimeSecondsOverride: exercise.targetTimeSecondsOverride,
|
||||
targetRepsOverride: exercise.targetRepsOverride,
|
||||
targetScoreOverride: exercise.targetScoreOverride,
|
||||
autoStartNextTimedStepOverride: exercise.autoStartNextTimedStepOverride,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -680,6 +721,8 @@ final class WorkoutTemplateExerciseDraft {
|
||||
required this.timeEnabled,
|
||||
required this.repsEnabled,
|
||||
required this.scoreEnabled,
|
||||
this.exerciseStepsSnapshot = const [],
|
||||
this.autoStartNextTimedStepSnapshot = true,
|
||||
this.scoreUnitSnapshot,
|
||||
this.targetTimeSeconds,
|
||||
this.targetReps,
|
||||
@ -689,6 +732,7 @@ final class WorkoutTemplateExerciseDraft {
|
||||
this.targetTimeSecondsOverride,
|
||||
this.targetRepsOverride,
|
||||
this.targetScoreOverride,
|
||||
this.autoStartNextTimedStepOverride,
|
||||
});
|
||||
|
||||
final String snapshotExerciseId;
|
||||
@ -697,6 +741,8 @@ final class WorkoutTemplateExerciseDraft {
|
||||
final bool timeEnabled;
|
||||
final bool repsEnabled;
|
||||
final bool scoreEnabled;
|
||||
final List<ExerciseStep> exerciseStepsSnapshot;
|
||||
final bool autoStartNextTimedStepSnapshot;
|
||||
final String? scoreUnitSnapshot;
|
||||
final int? targetTimeSeconds;
|
||||
final int? targetReps;
|
||||
@ -706,12 +752,18 @@ final class WorkoutTemplateExerciseDraft {
|
||||
int? targetTimeSecondsOverride;
|
||||
int? targetRepsOverride;
|
||||
double? targetScoreOverride;
|
||||
bool? autoStartNextTimedStepOverride;
|
||||
|
||||
bool get autoStartNextTimedStepEffective {
|
||||
return autoStartNextTimedStepOverride ?? autoStartNextTimedStepSnapshot;
|
||||
}
|
||||
|
||||
bool get hasOverride {
|
||||
return setsCountOverride != null ||
|
||||
targetTimeSecondsOverride != null ||
|
||||
targetRepsOverride != null ||
|
||||
targetScoreOverride != null;
|
||||
targetScoreOverride != null ||
|
||||
autoStartNextTimedStepOverride != null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -746,6 +798,10 @@ List<WorkoutTemplateExerciseDraft> _parseExercises(
|
||||
final rawExercises = snapshot['exercises'] as List<dynamic>? ?? const [];
|
||||
return rawExercises.map((raw) {
|
||||
final exercise = raw as Map<String, dynamic>;
|
||||
final autoStartNextTimedStepSnapshot =
|
||||
(exercise['autoStartNextTimedStepOverride'] as bool?) ??
|
||||
(exercise['autoStartNextTimedStepSnapshot'] as bool?) ??
|
||||
true;
|
||||
return WorkoutTemplateExerciseDraft(
|
||||
snapshotExerciseId: exercise['id'] as String,
|
||||
exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String,
|
||||
@ -753,6 +809,10 @@ List<WorkoutTemplateExerciseDraft> _parseExercises(
|
||||
timeEnabled: exercise['timeEnabled'] as bool,
|
||||
repsEnabled: exercise['repsEnabled'] as bool,
|
||||
scoreEnabled: exercise['scoreEnabled'] as bool,
|
||||
exerciseStepsSnapshot: _parseExerciseSteps(
|
||||
exercise['exerciseStepsSnapshot'],
|
||||
),
|
||||
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
|
||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||
targetTimeSeconds: exercise['targetTimeSeconds'] as int?,
|
||||
targetReps: exercise['targetReps'] as int?,
|
||||
@ -761,6 +821,34 @@ List<WorkoutTemplateExerciseDraft> _parseExercises(
|
||||
}).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) {
|
||||
final programCount = template.programs.length;
|
||||
final exerciseCount = template.programs.fold<int>(
|
||||
|
||||
@ -435,6 +435,58 @@ void main() {
|
||||
expect(saved.steps.single.defaultTargetValue, 12);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'le réglage d’enchaî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 l’indique visuellement', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@ -306,6 +306,165 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'personnaliser l’enchaînement automatique transmet l’override 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 l’exercice remet l’override 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 l’objectif de chrono pour un score en mode chrono', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@ -44,6 +44,117 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'personnaliser l’enchaînement automatique transmet l’override 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 l’override 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(
|
||||
'lancer une séance alors qu’une autre est en cours affiche un message',
|
||||
(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 {
|
||||
const _FakeClock(this.value);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user