feat(bibliotheque): tags de filtrage et duplication rapide côté UI (ticket #83)
Lot frontend F1+F2 : composant library_tags.dart (filtre/suggestions de tags), intégration dans exercise_library_screen, program_screen et workout_template_screen (tags + action duplication rapide), tests associés. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -21,6 +21,12 @@ void main() {
|
||||
);
|
||||
|
||||
await tester.enterText(find.byType(TextFormField).first, 'Gainage');
|
||||
await tester.dragUntilVisible(
|
||||
find.widgetWithText(SwitchListTile, 'Temps'),
|
||||
find.byType(ListView),
|
||||
const Offset(0, -200),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(SwitchListTile, 'Temps'));
|
||||
await tester.pump();
|
||||
await tester.dragUntilVisible(
|
||||
@ -73,6 +79,103 @@ void main() {
|
||||
expect(find.text('Exemple'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('ajouter un tag à un exercice le sauvegarde normalisé', (
|
||||
tester,
|
||||
) async {
|
||||
final exerciseRepository = _FakeExerciseRepository();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ExerciseFormScreen(
|
||||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||
mediaUseCases: _mediaUseCases(exerciseRepository),
|
||||
tagSuggestions: const ['match'],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Tirs');
|
||||
await tester.tap(find.text('Ajouter un tag'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Nom du tag'),
|
||||
' Match ',
|
||||
);
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Ajouter'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.widgetWithText(Chip, 'match'), findsOneWidget);
|
||||
|
||||
await tester.dragUntilVisible(
|
||||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
||||
find.byType(ListView),
|
||||
const Offset(0, -200),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
||||
'30',
|
||||
);
|
||||
await tester.dragUntilVisible(
|
||||
find.text('Enregistrer'),
|
||||
find.byType(ListView),
|
||||
const Offset(0, -200),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Enregistrer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(exerciseRepository.saved.single.tags, ['match']);
|
||||
});
|
||||
|
||||
testWidgets('filtre les exercices par tag et efface tous les filtres', (
|
||||
tester,
|
||||
) async {
|
||||
final exerciseRepository = _FakeExerciseRepository()
|
||||
..exercises.addAll([
|
||||
Exercise(
|
||||
metadata: _metadata('exercise-1'),
|
||||
name: 'Tirs match',
|
||||
hasTimeMeasure: true,
|
||||
hasRepsMeasure: false,
|
||||
hasScoreMeasure: false,
|
||||
tags: const ['match', 'extérieur'],
|
||||
),
|
||||
Exercise(
|
||||
metadata: _metadata('exercise-2'),
|
||||
name: 'Dribble routine',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
tags: const ['routine'],
|
||||
),
|
||||
]);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ExerciseLibraryScreen(
|
||||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||
mediaUseCases: _mediaUseCases(exerciseRepository),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.widgetWithText(FilterChip, 'match'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Tirs match'), findsOneWidget);
|
||||
expect(find.text('Dribble routine'), findsNothing);
|
||||
expect(find.text('Effacer les filtres'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Effacer les filtres'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Tirs match'), findsOneWidget);
|
||||
expect(find.text('Dribble routine'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('basculer en chrono intégré masque l’unité et affiche le badge', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@ -35,7 +35,7 @@ void main() {
|
||||
|
||||
expect(find.text('Fondations basket - 45 min'), findsOneWidget);
|
||||
expect(find.text('Exemple'), findsOneWidget);
|
||||
expect(find.byTooltip('Supprimer le programme'), findsOneWidget);
|
||||
expect(find.byTooltip('Actions'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('la liste programmes vide conserve le message dédié', (
|
||||
@ -65,6 +65,98 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('filtre les programmes par tag et peut effacer les filtres', (
|
||||
tester,
|
||||
) async {
|
||||
final programRepository = _FakeProgramRepository()
|
||||
..programs.addAll([
|
||||
Program(
|
||||
metadata: _metadata('program-1'),
|
||||
name: 'Programme match',
|
||||
defaultRestSeconds: 45,
|
||||
tags: const ['match', 'intense'],
|
||||
),
|
||||
Program(
|
||||
metadata: _metadata('program-2'),
|
||||
name: 'Programme routine',
|
||||
defaultRestSeconds: 30,
|
||||
tags: const ['routine'],
|
||||
),
|
||||
]);
|
||||
final exerciseRepository = _FakeExerciseRepository();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ProgramListScreen(
|
||||
programUseCases: _programUseCases(
|
||||
programRepository,
|
||||
exerciseRepository,
|
||||
),
|
||||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.widgetWithText(FilterChip, 'match'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.widgetWithText(FilterChip, 'match'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Programme match'), findsOneWidget);
|
||||
expect(find.text('Programme routine'), findsNothing);
|
||||
expect(find.text('Effacer les filtres'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Effacer les filtres'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Programme match'), findsOneWidget);
|
||||
expect(find.text('Programme routine'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('dupliquer un programme ouvre la copie en édition', (
|
||||
tester,
|
||||
) async {
|
||||
final programRepository = _FakeProgramRepository()
|
||||
..programs.add(
|
||||
Program(
|
||||
metadata: _metadata('program-1'),
|
||||
name: 'Jambes',
|
||||
defaultRestSeconds: 60,
|
||||
tags: const ['match'],
|
||||
),
|
||||
);
|
||||
final exerciseRepository = _FakeExerciseRepository();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ProgramListScreen(
|
||||
programUseCases: _programUseCases(
|
||||
programRepository,
|
||||
exerciseRepository,
|
||||
),
|
||||
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byTooltip('Actions'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Dupliquer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Modifier le programme'), findsOneWidget);
|
||||
expect(find.text('Copie de Jambes'), findsOneWidget);
|
||||
expect(
|
||||
find.text("Programme dupliqué. Ajuste la copie avant de l'utiliser."),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.widgetWithText(Chip, 'match'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('modifier un programme exemple conserve sa provenance', (
|
||||
tester,
|
||||
) async {
|
||||
@ -676,7 +768,9 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byTooltip('Supprimer le programme'));
|
||||
await tester.tap(find.byTooltip('Actions'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Supprimer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Supprimer Jambes ?'), findsOneWidget);
|
||||
@ -691,7 +785,9 @@ void main() {
|
||||
expect(find.text('Jambes'), findsOneWidget);
|
||||
expect(programRepository.saved, isEmpty);
|
||||
|
||||
await tester.tap(find.byTooltip('Supprimer le programme'));
|
||||
await tester.tap(find.byTooltip('Actions'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Supprimer'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Supprimer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@ -62,7 +62,7 @@ void main() {
|
||||
|
||||
expect(find.text('Séance jambes'), findsOneWidget);
|
||||
expect(find.text('Exemple'), findsOneWidget);
|
||||
expect(find.byTooltip('Supprimer la séance'), findsOneWidget);
|
||||
expect(find.byTooltip('Actions'), findsOneWidget);
|
||||
expect(find.text('Lancer'), findsOneWidget);
|
||||
});
|
||||
|
||||
@ -126,6 +126,143 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('filtre les séances par tag et peut effacer les filtres', (
|
||||
tester,
|
||||
) async {
|
||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||
final templateRepository = _FakeWorkoutTemplateRepository([
|
||||
_workoutTemplate(tags: const ['match', 'intense']),
|
||||
WorkoutTemplate(
|
||||
metadata: _metadata('template-2'),
|
||||
name: 'Séance routine',
|
||||
tags: const ['routine'],
|
||||
),
|
||||
]);
|
||||
final activeRepository = _FakeActiveSessionRepository();
|
||||
final programRepository = _FakeProgramRepository();
|
||||
final historyRepository = _FakeWorkoutHistoryRepository();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: WorkoutTemplateListScreen(
|
||||
workoutTemplateUseCases: WorkoutTemplateUseCases(
|
||||
templateRepository: templateRepository,
|
||||
programRepository: programRepository,
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
programUseCases: ProgramUseCases(
|
||||
programRepository: programRepository,
|
||||
exerciseRepository: _FakeExerciseRepository(),
|
||||
templateRepository: templateRepository,
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
activeUseCases: ActiveWorkoutSessionUseCases(
|
||||
sessionRepository: activeRepository,
|
||||
templateRepository: templateRepository,
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
closeUseCase: CloseWorkoutSessionUseCase(
|
||||
sessionRepository: activeRepository,
|
||||
historyRepository: historyRepository,
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
historyUseCases: WorkoutHistoryUseCases(
|
||||
repository: historyRepository,
|
||||
clock: clock,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.widgetWithText(FilterChip, 'match'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Séance jambes'), findsOneWidget);
|
||||
expect(find.text('Séance routine'), findsNothing);
|
||||
expect(find.text('Effacer les filtres'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Effacer les filtres'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Séance jambes'), findsOneWidget);
|
||||
expect(find.text('Séance routine'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('dupliquer une séance ouvre la copie en édition', (tester) async {
|
||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||
final templateRepository = _FakeWorkoutTemplateRepository([
|
||||
_workoutTemplate(tags: const ['match']),
|
||||
]);
|
||||
final activeRepository = _FakeActiveSessionRepository();
|
||||
final programRepository = _FakeProgramRepository();
|
||||
final historyRepository = _FakeWorkoutHistoryRepository();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: WorkoutTemplateListScreen(
|
||||
workoutTemplateUseCases: WorkoutTemplateUseCases(
|
||||
templateRepository: templateRepository,
|
||||
programRepository: programRepository,
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
programUseCases: ProgramUseCases(
|
||||
programRepository: programRepository,
|
||||
exerciseRepository: _FakeExerciseRepository(),
|
||||
templateRepository: templateRepository,
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
activeUseCases: ActiveWorkoutSessionUseCases(
|
||||
sessionRepository: activeRepository,
|
||||
templateRepository: templateRepository,
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
closeUseCase: CloseWorkoutSessionUseCase(
|
||||
sessionRepository: activeRepository,
|
||||
historyRepository: historyRepository,
|
||||
clock: clock,
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
),
|
||||
historyUseCases: WorkoutHistoryUseCases(
|
||||
repository: historyRepository,
|
||||
clock: clock,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byTooltip('Actions'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Dupliquer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Modifier la séance'), findsOneWidget);
|
||||
expect(find.text('Copie de Séance jambes'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Séance dupliquée. Ajuste la copie avant de la lancer.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.widgetWithText(Chip, 'match'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('modifier une séance exemple conserve sa provenance', (
|
||||
tester,
|
||||
) async {
|
||||
@ -433,7 +570,9 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byTooltip('Supprimer la séance'));
|
||||
await tester.tap(find.byTooltip('Actions'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Supprimer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Supprimer Séance jambes ?'), findsOneWidget);
|
||||
@ -445,7 +584,9 @@ void main() {
|
||||
expect(find.text('Séance jambes'), findsOneWidget);
|
||||
expect(templateRepository.saved, isEmpty);
|
||||
|
||||
await tester.tap(find.byTooltip('Supprimer la séance'));
|
||||
await tester.tap(find.byTooltip('Actions'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Supprimer'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Supprimer'));
|
||||
await tester.pumpAndSettle();
|
||||
@ -511,11 +652,15 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
WorkoutTemplate _workoutTemplate({bool isExample = false}) {
|
||||
WorkoutTemplate _workoutTemplate({
|
||||
bool isExample = false,
|
||||
List<String> tags = const [],
|
||||
}) {
|
||||
return WorkoutTemplate(
|
||||
metadata: _metadata('template-1'),
|
||||
name: 'Séance jambes',
|
||||
isExample: isExample,
|
||||
tags: tags,
|
||||
programs: [
|
||||
WorkoutTemplateProgram(
|
||||
metadata: _metadata('template-program-1'),
|
||||
|
||||
Reference in New Issue
Block a user