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:
2026-07-22 10:22:47 +02:00
parent e57cfdf54d
commit 1b5fc935bd
7 changed files with 1220 additions and 27 deletions

View File

@ -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'),