Ajoute la persistance de l'exécution par étapes avec résultats par passage (ports, use cases, entités, modèle Drift avec migration) et l'éditeur d'exercice avec séquence d'étapes sur exercise_library_screen.dart. Développés dans le même worktree partagé par DevBackend et DevFrontend ; commit combiné plutôt que séparé car les fakes de repository de test corrigés (nouvelles interfaces #58) touchent plusieurs écrans sans lien avec l'éditeur lui-même (historique, accueil, exécution, séance-modèle). Corrige au passage un ListTile sans ancêtre Material dans le nouvel éditeur d'étapes (Material(type: MaterialType.transparency)). flutter pub get OK, build_runner OK, analyze propre (mêmes infos préexistantes), 93/93 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -216,6 +216,204 @@ void main() {
|
||||
expect(find.text('Aucune image sélectionnée'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('activer les étapes permet d’ajouter une étape temps', (
|
||||
tester,
|
||||
) async {
|
||||
final exerciseRepository = _FakeExerciseRepository();
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(400, 3200));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
|
||||
await _pumpExerciseForm(tester, exerciseRepository);
|
||||
await tester.tap(
|
||||
find.widgetWithText(SwitchListTile, 'Rythmer cet exercice avec des étapes'),
|
||||
);
|
||||
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.pump();
|
||||
|
||||
expect(find.text('1. Dribble main droite'), findsOneWidget);
|
||||
expect(find.text('Temps · 12 s'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('ajouter une étape répétitions avec score manuel', (
|
||||
tester,
|
||||
) async {
|
||||
final exerciseRepository = _FakeExerciseRepository();
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(400, 3600));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
|
||||
await _pumpExerciseForm(tester, exerciseRepository);
|
||||
await tester.tap(
|
||||
find.widgetWithText(SwitchListTile, 'Rythmer cet exercice avec des étapes'),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Ajouter une étape'));
|
||||
await tester.pump();
|
||||
await tester.tap(_stepTypeRadio('Répétitions').last);
|
||||
await tester.pump();
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Nom de l’étape').last,
|
||||
'Pompes',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Répétitions par défaut').last,
|
||||
'10',
|
||||
);
|
||||
await tester.tap(find.widgetWithText(SwitchListTile, 'Score d’étape'));
|
||||
await tester.pump();
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Score à saisir').last,
|
||||
'Réussites',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Unité').last,
|
||||
'paniers',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Score par défaut').last,
|
||||
'0',
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('1. Pompes'), findsOneWidget);
|
||||
expect(find.text('Répétitions · 10'), findsOneWidget);
|
||||
expect(find.text('Mode de score'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('réordonner les étapes via Monter Descendre', (tester) async {
|
||||
final exerciseRepository = _FakeExerciseRepository();
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(400, 4200));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
|
||||
await _pumpExerciseForm(tester, exerciseRepository);
|
||||
await tester.tap(
|
||||
find.widgetWithText(SwitchListTile, 'Rythmer cet exercice avec des étapes'),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Ajouter une étape'));
|
||||
await tester.pump();
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Nom de l’étape').at(0),
|
||||
'Première',
|
||||
);
|
||||
await tester.tap(find.text('Ajouter une étape'));
|
||||
await tester.pump();
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Nom de l’étape').at(1),
|
||||
'Deuxième',
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byTooltip('Actions de l’étape 2'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Monter'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('1. Deuxième'), findsOneWidget);
|
||||
expect(find.text('2. Première'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('supprimer une étape demande confirmation', (tester) async {
|
||||
final exerciseRepository = _FakeExerciseRepository();
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(400, 3200));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
|
||||
await _pumpExerciseForm(tester, exerciseRepository);
|
||||
await tester.tap(
|
||||
find.widgetWithText(SwitchListTile, 'Rythmer cet exercice avec des étapes'),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Ajouter une étape'));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byTooltip('Actions de l’étape 1'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Supprimer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Supprimer cette étape ?'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Supprimer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Nouvelle étape'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('la limite de 8 étapes bloque l’ajout d’une neuvième', (
|
||||
tester,
|
||||
) async {
|
||||
final exerciseRepository = _FakeExerciseRepository();
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(400, 8000));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
|
||||
await _pumpExerciseForm(tester, exerciseRepository);
|
||||
await tester.tap(
|
||||
find.widgetWithText(SwitchListTile, 'Rythmer cet exercice avec des étapes'),
|
||||
);
|
||||
await tester.pump();
|
||||
for (var index = 0; index < 8; index++) {
|
||||
await tester.tap(find.text('Ajouter une étape'));
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
expect(find.text('Limite atteinte'), findsOneWidget);
|
||||
expect(find.text('Un exercice peut contenir jusqu’à 8 étapes.'), findsOneWidget);
|
||||
expect(find.text('Ajouter une étape'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('sauvegarder transmet les étapes au use case', (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.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.steps.single.name, 'Dribble main droite');
|
||||
expect(saved.steps.single.type, ExerciseStepType.time);
|
||||
expect(saved.steps.single.defaultTargetValue, 12);
|
||||
});
|
||||
|
||||
testWidgets('désigner une image comme icône l’indique visuellement', (
|
||||
tester,
|
||||
) async {
|
||||
@ -335,6 +533,30 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pumpExerciseForm(
|
||||
WidgetTester tester,
|
||||
_FakeExerciseRepository exerciseRepository,
|
||||
) {
|
||||
return tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ExerciseFormScreen(
|
||||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||
mediaUseCases: _mediaUseCases(exerciseRepository),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Finder _stepTypeRadio(String title) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is RadioListTile<ExerciseStepType> &&
|
||||
widget.title is Text &&
|
||||
(widget.title as Text).data == title,
|
||||
description: 'RadioListTile<ExerciseStepType> "$title"',
|
||||
);
|
||||
}
|
||||
|
||||
ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) {
|
||||
return ExerciseUseCases(
|
||||
repository: repository,
|
||||
|
||||
@ -274,6 +274,9 @@ final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(WorkoutHistorySetResult result) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveStepResult(WorkoutHistoryStepResult result) async {}
|
||||
}
|
||||
|
||||
final class _FakeWorkoutTemplateRepository
|
||||
@ -335,6 +338,14 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
required int setIndex,
|
||||
}) async => null;
|
||||
|
||||
@override
|
||||
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async => null;
|
||||
|
||||
@override
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return const [];
|
||||
@ -352,6 +363,20 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
|
||||
String sessionId,
|
||||
) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
|
||||
String sessionId,
|
||||
) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(ActiveWorkoutSession session) async {}
|
||||
|
||||
@ -361,6 +386,14 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
@override
|
||||
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveExerciseStepProgressState(
|
||||
ActiveExerciseStepProgressState state,
|
||||
) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {}
|
||||
|
||||
@override
|
||||
Future<void> deleteScoreStopwatchState({
|
||||
required String sessionId,
|
||||
|
||||
@ -267,6 +267,16 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return restStates;
|
||||
@ -284,6 +294,20 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
return results;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
|
||||
String sessionId,
|
||||
) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
|
||||
String sessionId,
|
||||
) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(ActiveWorkoutSession session) async {
|
||||
this.session = session;
|
||||
@ -299,6 +323,14 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
scoreStopwatchStates.add(state);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveExerciseStepProgressState(
|
||||
ActiveExerciseStepProgressState state,
|
||||
) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {}
|
||||
|
||||
@override
|
||||
Future<void> deleteScoreStopwatchState({
|
||||
required String sessionId,
|
||||
@ -416,4 +448,7 @@ final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(WorkoutHistorySetResult result) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveStepResult(WorkoutHistoryStepResult result) async {}
|
||||
}
|
||||
|
||||
@ -1085,6 +1085,8 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
final results = <ActiveSetResult>[];
|
||||
final restStates = <ActiveRestState>[];
|
||||
final scoreStopwatchStates = <ActiveScoreStopwatchState>[];
|
||||
final stepProgressStates = <ActiveExerciseStepProgressState>[];
|
||||
final stepResults = <ActiveExerciseStepResult>[];
|
||||
|
||||
@override
|
||||
Future<ActiveWorkoutSession?> findById(String id) async {
|
||||
@ -1123,6 +1125,25 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
for (final state in stepProgressStates) {
|
||||
if (state.activeWorkoutSessionId == sessionId &&
|
||||
state.programIndex == programIndex &&
|
||||
state.exerciseIndex == exerciseIndex &&
|
||||
state.setIndex == setIndex &&
|
||||
state.metadata.deletedAt == null) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return restStates
|
||||
@ -1144,6 +1165,24 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
return results;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
|
||||
String sessionId,
|
||||
) async {
|
||||
return stepProgressStates
|
||||
.where((state) => state.activeWorkoutSessionId == sessionId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
|
||||
String sessionId,
|
||||
) async {
|
||||
return stepResults
|
||||
.where((result) => result.activeWorkoutSessionId == sessionId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(ActiveWorkoutSession session) async {
|
||||
this.session = session;
|
||||
@ -1173,6 +1212,44 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveExerciseStepProgressState(
|
||||
ActiveExerciseStepProgressState state,
|
||||
) async {
|
||||
final index = stepProgressStates.indexWhere(
|
||||
(saved) =>
|
||||
saved.activeWorkoutSessionId == state.activeWorkoutSessionId &&
|
||||
saved.programIndex == state.programIndex &&
|
||||
saved.exerciseIndex == state.exerciseIndex &&
|
||||
saved.setIndex == state.setIndex,
|
||||
);
|
||||
if (index == -1) {
|
||||
stepProgressStates.add(state);
|
||||
} else {
|
||||
stepProgressStates[index] = state;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveExerciseStepResult(
|
||||
ActiveExerciseStepResult result,
|
||||
) async {
|
||||
final index = stepResults.indexWhere(
|
||||
(saved) =>
|
||||
saved.activeWorkoutSessionId == result.activeWorkoutSessionId &&
|
||||
saved.programIndex == result.programIndex &&
|
||||
saved.exerciseIndex == result.exerciseIndex &&
|
||||
saved.setIndex == result.setIndex &&
|
||||
saved.passageIndex == result.passageIndex &&
|
||||
saved.stepIndex == result.stepIndex,
|
||||
);
|
||||
if (index == -1) {
|
||||
stepResults.add(result);
|
||||
} else {
|
||||
stepResults[index] = result;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteScoreStopwatchState({
|
||||
required String sessionId,
|
||||
@ -1246,6 +1323,9 @@ final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(WorkoutHistorySetResult result) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveStepResult(WorkoutHistoryStepResult result) async {}
|
||||
}
|
||||
|
||||
final class _FakeProgramRepository implements ProgramRepository {
|
||||
|
||||
@ -342,6 +342,16 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return const [];
|
||||
@ -359,6 +369,20 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
|
||||
String sessionId,
|
||||
) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
|
||||
String sessionId,
|
||||
) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(ActiveWorkoutSession session) async {
|
||||
saveCount += 1;
|
||||
@ -371,6 +395,14 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||
@override
|
||||
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveExerciseStepProgressState(
|
||||
ActiveExerciseStepProgressState state,
|
||||
) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {}
|
||||
|
||||
@override
|
||||
Future<void> deleteScoreStopwatchState({
|
||||
required String sessionId,
|
||||
@ -430,4 +462,7 @@ final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(WorkoutHistorySetResult result) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveStepResult(WorkoutHistoryStepResult result) async {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user