merge(main): validation exercices à étapes, persistance et historique (ticket #62)

Fusionne feature/exercise-steps-62-qa — analyze propre (mêmes infos
préexistantes), 103/103 tests verts, build APK debug validé. Corrige
un vrai bug trouvé en QA (suppression d'exercice cascadant à tort
vers les programmes) et le texte de confirmation UI devenu incohérent
avec ce fix. Clôture le chantier "Exercice à plusieurs étapes" (#54,
tickets #56/#58/#59/#60/#61/#62).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 20:56:42 +02:00
7 changed files with 193 additions and 124 deletions

View File

@ -1,6 +1,9 @@
--- ---
issueRef: "#61" issueRef: "#61"
version: 1 version: 3
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784481541650 updatedAt: 1784485187502
--- ---
Plan de séance : résumé de séquence par série (via `ActiveExerciseStepUseCases.readProgress`). Historique : détail des résultats d'étapes groupés par passage (durée/répétitions, score d'étape, badge "Passée" pour skipped), réutilise `WorkoutHistory.stepResults` déjà peuplé par #58. 102/102 tests verts, `flutter analyze` clean, APK debug buildé avec succès.
Écart potentiel vs description initiale du ticket (rédigée par Architect) : la description mentionne aussi "l'édition ponctuelle d'une série passée/terminée permet de corriger les valeurs enregistrées [d'étapes] sans rejouer la séquence interactive" — mon brief à DevFrontend n'a couvert que l'AFFICHAGE des résultats d'étapes dans le plan/historique, pas leur édition a posteriori. Le flux existant "modifier une série passée" (upsertSetResultAtPosition) n'a pas été étendu pour éditer les résultats d'étapes individuels. À vérifier en QA (#62) si c'est bloquant pour l'usage réel ou si un ticket de suivi séparé suffit.

View File

@ -2,15 +2,15 @@
id: "1eed02ec-04af-4966-a0ff-449c4a957ea0" id: "1eed02ec-04af-4966-a0ff-449c4a957ea0"
number: 61 number: 61
title: "[DevFrontend] Plan de séance et historique avec résultats d'étapes" title: "[DevFrontend] Plan de séance et historique avec résultats d'étapes"
status: "open" status: "qa"
priority: "medium" priority: "medium"
sprint: null sprint: null
links: [{"target":"#54","kind":"relatesTo"},{"target":"#58","kind":"dependsOn"}] links: [{"target":"#54","kind":"relatesTo"},{"target":"#58","kind":"dependsOn"}]
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}] agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784481541650 createdAt: 1784481541650
updatedAt: 1784481541650 updatedAt: 1784485187502
version: 1 version: 3
--- ---
Afficher les résultats d'étapes dans le plan de séance et l'historique sans remplacer les résultats de série. Une série avec étapes garde son état global `à faire/en cours/terminée/passée`; le détail affiche les passages, étapes terminées/passées, durées/répétitions et scores d'étape. L'édition ponctuelle d'une série passée/terminée permet de corriger les valeurs enregistrées sans rejouer la séquence interactive et sans déplacer la progression courante. Afficher les résultats d'étapes dans le plan de séance et l'historique sans remplacer les résultats de série. Une série avec étapes garde son état global `à faire/en cours/terminée/passée`; le détail affiche les passages, étapes terminées/passées, durées/répétitions et scores d'étape. L'édition ponctuelle d'une série passée/terminée permet de corriger les valeurs enregistrées sans rejouer la séquence interactive et sans déplacer la progression courante.

View File

@ -679,13 +679,13 @@
"issueRef": "#61", "issueRef": "#61",
"path": "61", "path": "61",
"title": "[DevFrontend] Plan de séance et historique avec résultats d'étapes", "title": "[DevFrontend] Plan de séance et historique avec résultats d'étapes",
"status": "open", "status": "qa",
"priority": "medium", "priority": "medium",
"sprint": null, "sprint": null,
"assignedAgentIds": [ "assignedAgentIds": [
"9933c93a-b8a1-4164-a3bb-7063fdad747d" "9933c93a-b8a1-4164-a3bb-7063fdad747d"
], ],
"updatedAt": 1784481541650 "updatedAt": 1784485187502
}, },
{ {
"issueRef": "#62", "issueRef": "#62",

View File

@ -291,22 +291,6 @@ final class ExerciseUseCases {
final deleted = exercise.copyWith( final deleted = exercise.copyWith(
metadata: exercise.metadata.markDeleted(now), metadata: exercise.metadata.markDeleted(now),
); );
final programs = await programRepository.listActive();
for (final program in programs) {
final keptExercises = program.exercises
.where((programExercise) => programExercise.sourceExerciseId != id)
.toList();
if (keptExercises.length == program.exercises.length) {
continue;
}
await programRepository.replaceExercises(
program.copyWith(
metadata: program.metadata.touch(now),
exercises: _repositionProgramExercises(keptExercises, now),
),
now,
);
}
await repository.save(deleted); await repository.save(deleted);
return deleted; return deleted;
} }
@ -964,12 +948,12 @@ final class ActiveWorkoutSessionUseCases {
await sessionRepository.saveScoreStopwatchState(stopped); await sessionRepository.saveScoreStopwatchState(stopped);
} }
final runningStepTimers = final runningStepTimers =
(await sessionRepository.listExerciseStepProgressStates(sessionId)) (await sessionRepository.listExerciseStepProgressStates(
.where( sessionId,
(state) => )).where(
state.status == (state) =>
ActiveExerciseStepProgressStatus.runningTimer, state.status == ActiveExerciseStepProgressStatus.runningTimer,
); );
for (final state in runningStepTimers) { for (final state in runningStepTimers) {
final pausedStep = state.copyWith( final pausedStep = state.copyWith(
metadata: state.metadata.touch(now), metadata: state.metadata.touch(now),
@ -1510,7 +1494,8 @@ final class ActiveExerciseStepUseCases {
exerciseIndex: exerciseIndex, exerciseIndex: exerciseIndex,
setIndex: setIndex, setIndex: setIndex,
); );
final state = existing ?? final state =
existing ??
_initialStepProgressState( _initialStepProgressState(
sessionId: sessionId, sessionId: sessionId,
context: context, context: context,
@ -2158,47 +2143,6 @@ final class SetResultPositionState {
final ActiveSetResult? result; final ActiveSetResult? result;
} }
List<ProgramExercise> _repositionProgramExercises(
List<ProgramExercise> exercises,
DateTime now,
) {
return [
for (var index = 0; index < exercises.length; index++)
ProgramExercise(
metadata: exercises[index].metadata.touch(now),
programId: exercises[index].programId,
sourceExerciseId: exercises[index].sourceExerciseId,
position: index,
exerciseNameSnapshot: exercises[index].exerciseNameSnapshot,
exerciseDescriptionSnapshot:
exercises[index].exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot:
exercises[index].exerciseImageMediaIdSnapshot,
exerciseImageMediaIdsSnapshot:
exercises[index].exerciseImageMediaIdsSnapshot,
exerciseVideoMediaIdSnapshot:
exercises[index].exerciseVideoMediaIdSnapshot,
exerciseStepsSnapshot: exercises[index].exerciseStepsSnapshot,
exerciseArchivedSnapshot: exercises[index].exerciseArchivedSnapshot,
availableTimeSnapshot: exercises[index].availableTimeSnapshot,
availableRepsSnapshot: exercises[index].availableRepsSnapshot,
availableScoreSnapshot: exercises[index].availableScoreSnapshot,
scoreInputModeSnapshot: exercises[index].scoreInputModeSnapshot,
scoreLabelSnapshot: exercises[index].scoreLabelSnapshot,
scoreUnitSnapshot: exercises[index].scoreUnitSnapshot,
setsCount: exercises[index].setsCount,
timeEnabled: exercises[index].timeEnabled,
repsEnabled: exercises[index].repsEnabled,
scoreEnabled: exercises[index].scoreEnabled,
targetTimeSeconds: exercises[index].targetTimeSeconds,
targetReps: exercises[index].targetReps,
targetScore: exercises[index].targetScore,
targetScoreTimeMs: exercises[index].targetScoreTimeMs,
restSecondsOverride: exercises[index].restSecondsOverride,
),
];
}
List<WorkoutTemplateProgram> _repositionWorkoutTemplatePrograms( List<WorkoutTemplateProgram> _repositionWorkoutTemplatePrograms(
List<WorkoutTemplateProgram> programs, List<WorkoutTemplateProgram> programs,
DateTime now, DateTime now,
@ -2494,7 +2438,8 @@ _ResolvedExerciseSnapshot? _findExerciseSnapshot({
} }
final programSnapshot = final programSnapshot =
jsonDecode(programSnapshotJson) as Map<String, dynamic>; jsonDecode(programSnapshotJson) as Map<String, dynamic>;
final exercises = (programSnapshot['exercises'] as List<dynamic>? ?? const []); final exercises =
(programSnapshot['exercises'] as List<dynamic>? ?? const []);
if (exerciseIndex < 0 || exerciseIndex >= exercises.length) { if (exerciseIndex < 0 || exerciseIndex >= exercises.length) {
return null; return null;
} }
@ -2749,26 +2694,28 @@ List<ExerciseStep> _exerciseStepsFromSnapshot(Object? value) {
if (value is! List) { if (value is! List) {
return const []; return const [];
} }
return value.map((item) { return value
final json = Map<String, dynamic>.from(item as Map); .map((item) {
return ExerciseStep( final json = Map<String, dynamic>.from(item as Map);
id: json['id'] as String, return ExerciseStep(
position: json['position'] as int, id: json['id'] as String,
name: json['name'] as String, position: json['position'] as int,
type: switch (json['type']) { name: json['name'] as String,
'time' => ExerciseStepType.time, type: switch (json['type']) {
'reps' => ExerciseStepType.reps, 'time' => ExerciseStepType.time,
_ => throw const DomainException('Unknown exercise step type.'), 'reps' => ExerciseStepType.reps,
}, _ => throw const DomainException('Unknown exercise step type.'),
defaultTargetValue: json['defaultTargetValue'] as int, },
hasScore: json['hasScore'] == true, defaultTargetValue: json['defaultTargetValue'] as int,
scoreInputMode: _scoreInputModeFromSnapshot(json['scoreInputMode']), hasScore: json['hasScore'] == true,
scoreLabel: json['scoreLabel'] as String?, scoreInputMode: _scoreInputModeFromSnapshot(json['scoreInputMode']),
scoreUnit: json['scoreUnit'] as String?, scoreLabel: json['scoreLabel'] as String?,
defaultTargetScore: (json['defaultTargetScore'] as num?)?.toDouble(), scoreUnit: json['scoreUnit'] as String?,
defaultTargetScoreTimeMs: json['defaultTargetScoreTimeMs'] as int?, defaultTargetScore: (json['defaultTargetScore'] as num?)?.toDouble(),
); defaultTargetScoreTimeMs: json['defaultTargetScoreTimeMs'] as int?,
}).toList(growable: false); );
})
.toList(growable: false);
} }
List<WorkoutHistoryStepResult> _historyStepResultsFromActiveResults({ List<WorkoutHistoryStepResult> _historyStepResultsFromActiveResults({

View File

@ -220,7 +220,8 @@ final class _ExerciseLibraryScreenState extends State<ExerciseLibraryScreen> {
content: Text( content: Text(
used used
? 'Cet exercice est utilisé dans un ou plusieurs programmes. ' ? 'Cet exercice est utilisé dans un ou plusieurs programmes. '
'Le supprimer le retirera de ces programmes. Continuer ?' 'Il sera retiré de la bibliothèque. '
'Les programmes existants restent inchangés. Continuer ?'
: 'Cette action est irréversible.', : 'Cette action est irréversible.',
), ),
actions: [ actions: [
@ -885,8 +886,9 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
controller: draft.targetController, controller: draft.targetController,
decoration: InputDecoration(labelText: targetLabel), decoration: InputDecoration(labelText: targetLabel),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
validator: (value) => validator: (value) => _stepsEnabled
_stepsEnabled ? _positiveIntValidator(value, targetMessage) : null, ? _positiveIntValidator(value, targetMessage)
: null,
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -898,7 +900,10 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
onChanged: (value) => setState(() => draft.hasScore = value), onChanged: (value) => setState(() => draft.hasScore = value),
), ),
if (draft.hasScore) ...[ if (draft.hasScore) ...[
Text('Mode de score', style: Theme.of(context).textTheme.titleSmall), Text(
'Mode de score',
style: Theme.of(context).textTheme.titleSmall,
),
RadioListTile<ScoreInputMode>( RadioListTile<ScoreInputMode>(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: const Text('Saisie libre'), title: const Text('Saisie libre'),
@ -922,7 +927,9 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
if (draft.scoreInputMode == ScoreInputMode.manual) ...[ if (draft.scoreInputMode == ScoreInputMode.manual) ...[
TextFormField( TextFormField(
controller: draft.scoreLabelController, controller: draft.scoreLabelController,
decoration: const InputDecoration(labelText: 'Score à saisir'), decoration: const InputDecoration(
labelText: 'Score à saisir',
),
validator: (value) { validator: (value) {
if (!_stepsEnabled || !draft.hasScore) return null; if (!_stepsEnabled || !draft.hasScore) return null;
if (draft.scoreInputMode != ScoreInputMode.manual) { if (draft.scoreInputMode != ScoreInputMode.manual) {
@ -1081,9 +1088,10 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
} }
setState(() { setState(() {
_stepError = null; _stepError = null;
_stepDrafts.insert(index + 1, _stepDrafts[index].duplicate( _stepDrafts.insert(
'step-draft-${_nextStepDraftId++}', index + 1,
)); _stepDrafts[index].duplicate('step-draft-${_nextStepDraftId++}'),
);
}); });
} }
@ -1143,8 +1151,8 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
for (var index = 0; index < _stepDrafts.length; index++) for (var index = 0; index < _stepDrafts.length; index++)
_stepDrafts[index].toExerciseStep( _stepDrafts[index].toExerciseStep(
position: index, position: index,
defaultTargetScoreTimeMs: _stepDrafts[index].scoreInputMode == defaultTargetScoreTimeMs:
ScoreInputMode.stopwatch _stepDrafts[index].scoreInputMode == ScoreInputMode.stopwatch
? _optionalSecondsToMilliseconds( ? _optionalSecondsToMilliseconds(
_stepDrafts[index].scoreTargetController.text, _stepDrafts[index].scoreTargetController.text,
) )

View File

@ -479,7 +479,7 @@ void main() {
); );
}); });
test('delete exercise removes it from referencing programs', () async { test('delete exercise keeps existing program snapshots unchanged', () async {
final exerciseRepository = _FakeExerciseRepository() final exerciseRepository = _FakeExerciseRepository()
..exercise = Exercise( ..exercise = Exercise(
metadata: _metadata('exercise-1'), metadata: _metadata('exercise-1'),
@ -502,12 +502,6 @@ void main() {
sourceExerciseId: 'exercise-1', sourceExerciseId: 'exercise-1',
position: 0, position: 0,
), ),
_programExercise(
id: 'program-exercise-2',
programId: 'program-1',
sourceExerciseId: 'exercise-2',
position: 1,
),
], ],
), ),
); );
@ -523,7 +517,11 @@ void main() {
expect(programRepository.programs.single.exercises, hasLength(1)); expect(programRepository.programs.single.exercises, hasLength(1));
expect( expect(
programRepository.programs.single.exercises.single.sourceExerciseId, programRepository.programs.single.exercises.single.sourceExerciseId,
'exercise-2', 'exercise-1',
);
expect(
programRepository.programs.single.exercises.single.exerciseNameSnapshot,
'Exercise program-exercise-1',
); );
expect(programRepository.programs.single.exercises.single.position, 0); expect(programRepository.programs.single.exercises.single.position, 0);
}); });
@ -1020,6 +1018,95 @@ void main() {
expect(stopped.accumulatedMs, 4000); expect(stopped.accumulatedMs, 4000);
}); });
test(
'exercise step progress auto-advances consecutive elapsed timers after kill',
() async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final session = _sessionWithSnapshot(
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
setsCount: 1,
targetReps: 2,
exerciseSteps: [
_step(
id: 'step-1',
position: 0,
name: 'Gauche',
type: ExerciseStepType.time,
defaultTargetValue: 1,
),
_step(
id: 'step-2',
position: 1,
name: 'Droite',
type: ExerciseStepType.time,
defaultTargetValue: 1,
),
],
);
final repository = _FakeActiveSessionRepository()..session = session;
final useCase = ActiveExerciseStepUseCases(
sessionRepository: repository,
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
);
await useCase.startOrResumeProgress(
sessionId: session.metadata.id,
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
);
await useCase.startTimer(
sessionId: session.metadata.id,
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
);
clock.value = clock.value.add(const Duration(milliseconds: 4500));
final reconstructed = ActiveExerciseStepUseCases(
sessionRepository: repository,
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
);
final view = await reconstructed.startOrResumeProgress(
sessionId: session.metadata.id,
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
);
expect(
view.state.status,
ActiveExerciseStepProgressStatus.sequenceComplete,
);
expect(repository.stepResults, hasLength(4));
expect(repository.stepResults.map((result) => result.passageIndex), [
0,
0,
1,
1,
]);
expect(repository.stepResults.map((result) => result.stepIndex), [
0,
1,
0,
1,
]);
expect(repository.stepResults.map((result) => result.actualTimeMs), [
1000,
1000,
1000,
1000,
]);
},
);
test('score result enforces manual xor stopwatch values', () { test('score result enforces manual xor stopwatch values', () {
expect( expect(
() => ActiveSetResult( () => ActiveSetResult(
@ -1431,9 +1518,7 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
} }
@override @override
Future<void> saveExerciseStepResult( Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {
ActiveExerciseStepResult result,
) async {
stepResults.removeWhere( stepResults.removeWhere(
(existing) => (existing) =>
existing.activeWorkoutSessionId == result.activeWorkoutSessionId && existing.activeWorkoutSessionId == result.activeWorkoutSessionId &&
@ -1484,6 +1569,8 @@ ActiveWorkoutSession _sessionWithSnapshot({
required int currentExerciseIndex, required int currentExerciseIndex,
required int currentSetIndex, required int currentSetIndex,
int setsCount = 2, int setsCount = 2,
int targetReps = 10,
List<ExerciseStep> exerciseSteps = const [],
}) { }) {
return ActiveWorkoutSession( return ActiveWorkoutSession(
metadata: _metadata('session-1'), metadata: _metadata('session-1'),
@ -1508,7 +1595,10 @@ ActiveWorkoutSession _sessionWithSnapshot({
'timeEnabled': false, 'timeEnabled': false,
'repsEnabled': true, 'repsEnabled': true,
'scoreEnabled': false, 'scoreEnabled': false,
'targetReps': 10, 'targetReps': targetReps,
'exerciseStepsSnapshot': exerciseSteps
.map((step) => step.toSnapshotJson())
.toList(),
}, },
], ],
}), }),

View File

@ -226,7 +226,10 @@ void main() {
await _pumpExerciseForm(tester, exerciseRepository); await _pumpExerciseForm(tester, exerciseRepository);
await tester.tap( await tester.tap(
find.widgetWithText(SwitchListTile, 'Rythmer cet exercice avec des étapes'), find.widgetWithText(
SwitchListTile,
'Rythmer cet exercice avec des étapes',
),
); );
await tester.pump(); await tester.pump();
await tester.tap(find.text('Ajouter une étape')); await tester.tap(find.text('Ajouter une étape'));
@ -256,7 +259,10 @@ void main() {
await _pumpExerciseForm(tester, exerciseRepository); await _pumpExerciseForm(tester, exerciseRepository);
await tester.tap( await tester.tap(
find.widgetWithText(SwitchListTile, 'Rythmer cet exercice avec des étapes'), find.widgetWithText(
SwitchListTile,
'Rythmer cet exercice avec des étapes',
),
); );
await tester.pump(); await tester.pump();
await tester.tap(find.text('Ajouter une étape')); await tester.tap(find.text('Ajouter une étape'));
@ -300,7 +306,10 @@ void main() {
await _pumpExerciseForm(tester, exerciseRepository); await _pumpExerciseForm(tester, exerciseRepository);
await tester.tap( await tester.tap(
find.widgetWithText(SwitchListTile, 'Rythmer cet exercice avec des étapes'), find.widgetWithText(
SwitchListTile,
'Rythmer cet exercice avec des étapes',
),
); );
await tester.pump(); await tester.pump();
await tester.tap(find.text('Ajouter une étape')); await tester.tap(find.text('Ajouter une étape'));
@ -334,7 +343,10 @@ void main() {
await _pumpExerciseForm(tester, exerciseRepository); await _pumpExerciseForm(tester, exerciseRepository);
await tester.tap( await tester.tap(
find.widgetWithText(SwitchListTile, 'Rythmer cet exercice avec des étapes'), find.widgetWithText(
SwitchListTile,
'Rythmer cet exercice avec des étapes',
),
); );
await tester.pump(); await tester.pump();
await tester.tap(find.text('Ajouter une étape')); await tester.tap(find.text('Ajouter une étape'));
@ -363,7 +375,10 @@ void main() {
await _pumpExerciseForm(tester, exerciseRepository); await _pumpExerciseForm(tester, exerciseRepository);
await tester.tap( await tester.tap(
find.widgetWithText(SwitchListTile, 'Rythmer cet exercice avec des étapes'), find.widgetWithText(
SwitchListTile,
'Rythmer cet exercice avec des étapes',
),
); );
await tester.pump(); await tester.pump();
for (var index = 0; index < 8; index++) { for (var index = 0; index < 8; index++) {
@ -372,7 +387,10 @@ void main() {
} }
expect(find.text('Limite atteinte'), findsOneWidget); expect(find.text('Limite atteinte'), findsOneWidget);
expect(find.text('Un exercice peut contenir jusquà 8 étapes.'), findsOneWidget); expect(
find.text('Un exercice peut contenir jusquà 8 étapes.'),
findsOneWidget,
);
expect(find.text('Ajouter une étape'), findsNothing); expect(find.text('Ajouter une étape'), findsNothing);
}); });
@ -389,7 +407,10 @@ void main() {
'30', '30',
); );
await tester.tap( await tester.tap(
find.widgetWithText(SwitchListTile, 'Rythmer cet exercice avec des étapes'), find.widgetWithText(
SwitchListTile,
'Rythmer cet exercice avec des étapes',
),
); );
await tester.pump(); await tester.pump();
await tester.tap(find.text('Ajouter une étape')); await tester.tap(find.text('Ajouter une étape'));
@ -512,7 +533,7 @@ void main() {
expect(find.text('Supprimer Squat ?'), findsOneWidget); expect(find.text('Supprimer Squat ?'), findsOneWidget);
expect( expect(
find.textContaining('Le supprimer le retirera de ces programmes.'), find.textContaining('Les programmes existants restent inchangés.'),
findsOneWidget, findsOneWidget,
); );