Files
GameTime/test/presentation/program_screen_test.dart
Blomios e05405157a feat(bibliothèque): badge Exemple, catégories et états vides bibliothèque (ticket #80)
Ajoute le badge "Exemple" (example_badge.dart) affiché sur les
exercices/programmes/séances-modèles seedés, le filtrage par catégorie
dans la bibliothèque d'exercices et les états vides dédiés. Préserve
isExample lors de l'édition d'un programme ou d'une séance-modèle
existants (ProgramUseCases/WorkoutTemplateUseCases).

Validé GO par Main : dart analyze propre, 173 tests (4 échecs
préexistants sans rapport avec ce lot).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:39:26 +02:00

1123 lines
32 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gametime/application/application.dart';
import 'package:gametime/domain/domain.dart';
import 'package:gametime/presentation/presentation.dart';
void main() {
testWidgets('la liste affiche le badge exemple des programmes seedés', (
tester,
) async {
final programRepository = _FakeProgramRepository()
..programs.add(
Program(
metadata: _metadata('program-1'),
name: 'Fondations basket - 45 min',
defaultRestSeconds: 45,
isExample: true,
),
);
final exerciseRepository = _FakeExerciseRepository();
await tester.pumpWidget(
MaterialApp(
home: ProgramListScreen(
programUseCases: _programUseCases(
programRepository,
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
),
),
);
await tester.pump();
await tester.pump();
expect(find.text('Fondations basket - 45 min'), findsOneWidget);
expect(find.text('Exemple'), findsOneWidget);
expect(find.byTooltip('Supprimer le programme'), findsOneWidget);
});
testWidgets('la liste programmes vide conserve le message dédié', (
tester,
) async {
final programRepository = _FakeProgramRepository();
final exerciseRepository = _FakeExerciseRepository();
await tester.pumpWidget(
MaterialApp(
home: ProgramListScreen(
programUseCases: _programUseCases(
programRepository,
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
),
),
);
await tester.pump();
await tester.pump();
expect(find.text('Aucun programme'), findsOneWidget);
expect(
find.text('Assemble quelques exercices pour préparer une séance.'),
findsOneWidget,
);
});
testWidgets('modifier un programme exemple conserve sa provenance', (
tester,
) async {
final programRepository = _FakeProgramRepository()
..programs.add(
Program(
metadata: _metadata('program-1'),
name: 'Fondations basket - 45 min',
defaultRestSeconds: 45,
isExample: true,
),
);
final exerciseRepository = _FakeExerciseRepository();
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
programRepository,
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
program: programRepository.programs.single,
),
),
);
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Edité');
await tester.tap(find.text('Enregistrer'));
await tester.pumpAndSettle();
expect(programRepository.programs.single.name, 'Edité');
expect(programRepository.programs.single.isExample, isTrue);
});
testWidgets(
'ajoute un exercice au programme avec sa configuration par défaut',
(tester) async {
final programRepository = _FakeProgramRepository();
final exerciseRepository = _FakeExerciseRepository()
..exercises.add(
Exercise(
metadata: _metadata('exercise-1'),
name: 'Squat',
hasTimeMeasure: true,
hasRepsMeasure: true,
hasScoreMeasure: false,
defaultTargetTimeSeconds: 45,
defaultTargetReps: 10,
),
);
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
programRepository,
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
),
),
);
await tester.enterText(
find.widgetWithText(TextFormField, 'Nom'),
'Jambes',
);
await tester.tap(find.text('Ajouter un exercice'));
await tester.pumpAndSettle();
await tester.tap(find.text('Squat'));
await tester.pumpAndSettle();
expect(find.text('Squat'), findsOneWidget);
expect(find.text('Repos 60 s'), findsOneWidget);
expect(find.text('Exercice ajouté'), findsOneWidget);
expect(find.byTooltip("Personnaliser l'exercice"), findsOneWidget);
expect(find.byType(CheckboxListTile), findsNothing);
await tester.tap(find.text('Enregistrer'));
await tester.pumpAndSettle();
final savedExercise = programRepository.programs.single.exercises.single;
expect(savedExercise.setsCount, 3);
expect(savedExercise.timeEnabled, isTrue);
expect(savedExercise.repsEnabled, isTrue);
expect(savedExercise.scoreEnabled, isFalse);
expect(savedExercise.targetTimeSeconds, 45);
expect(savedExercise.targetReps, 10);
expect(savedExercise.restSecondsOverride, 60);
},
);
testWidgets(
'limite les mesures aux mesures autorisées par lexercice source',
(tester) async {
final exerciseRepository = _FakeExerciseRepository()
..exercises.add(
Exercise(
metadata: _metadata('exercise-1'),
name: 'Pompes',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
defaultTargetReps: 12,
),
);
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
_FakeProgramRepository(),
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
),
),
);
await tester.tap(find.text('Ajouter un exercice'));
await tester.pumpAndSettle();
await tester.tap(find.text('Pompes'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip("Personnaliser l'exercice"));
await tester.pumpAndSettle();
expect(find.text("Personnaliser l'exercice"), findsOneWidget);
expect(
find.widgetWithText(SwitchListTile, 'Répétitions'),
findsOneWidget,
);
expect(find.widgetWithText(SwitchListTile, 'Temps'), findsNothing);
expect(find.widgetWithText(SwitchListTile, 'Score'), findsNothing);
},
);
testWidgets(
'la popup exercice ajouté disparaît et se ferme à la personnalisation',
(tester) async {
final exerciseRepository = _FakeExerciseRepository()
..exercises.add(
Exercise(
metadata: _metadata('exercise-1'),
name: 'Squat',
hasTimeMeasure: true,
hasRepsMeasure: false,
hasScoreMeasure: false,
defaultTargetTimeSeconds: 45,
),
);
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
_FakeProgramRepository(),
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
),
),
);
await tester.tap(find.text('Ajouter un exercice'));
await tester.pumpAndSettle();
await tester.tap(find.text('Squat'));
await tester.pumpAndSettle();
expect(find.text('Exercice ajouté'), findsOneWidget);
for (var i = 0; i < 5; i++) {
await tester.pump(const Duration(seconds: 1));
}
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('Exercice ajouté'), findsNothing);
await tester.tap(find.text('Ajouter un exercice'));
await tester.pumpAndSettle();
await tester.tap(find.text('Squat').first);
await tester.pumpAndSettle();
expect(find.text('Exercice ajouté'), findsOneWidget);
await tester.tap(find.text('Personnaliser'));
await tester.pumpAndSettle();
expect(find.text('Exercice ajouté'), findsNothing);
expect(find.text("Personnaliser l'exercice"), findsOneWidget);
},
);
testWidgets(
'ajouter un exercice avec étapes conserve le snapshot des étapes',
(tester) async {
final steps = [
_step(
id: 'step-1',
position: 0,
name: 'Course',
type: ExerciseStepType.time,
),
_step(
id: 'step-2',
position: 1,
name: 'Tirs',
type: ExerciseStepType.reps,
),
];
final programRepository = _FakeProgramRepository();
final exerciseRepository = _FakeExerciseRepository()
..exercises.add(
Exercise(
metadata: _metadata('exercise-steps'),
name: 'Circuit tirs',
hasTimeMeasure: false,
hasRepsMeasure: true,
hasScoreMeasure: false,
defaultTargetReps: 2,
steps: steps,
),
);
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('Circuit tirs'));
await tester.pumpAndSettle();
await tester.tap(find.text('Enregistrer'));
await tester.pumpAndSettle();
final savedSteps = programRepository
.programs
.single
.exercises
.single
.exerciseStepsSnapshot;
expect(savedSteps, hasLength(2));
expect(savedSteps.map((step) => step.name), ['Course', 'Tirs']);
expect(savedSteps.map((step) => step.type), [
ExerciseStepType.time,
ExerciseStepType.reps,
]);
},
);
testWidgets(
'réenregistrer un programme existant conserve les étapes du snapshot',
(tester) async {
final steps = [
_step(
id: 'step-1',
position: 0,
name: 'Course',
type: ExerciseStepType.time,
),
_step(
id: 'step-2',
position: 1,
name: 'Tirs',
type: ExerciseStepType.reps,
),
];
final programRepository = _FakeProgramRepository()
..programs.add(
Program(
metadata: _metadata('program-steps'),
name: 'Programme étapes',
defaultRestSeconds: 45,
exercises: [
ProgramExercise(
metadata: _metadata('program-exercise-steps'),
programId: 'program-steps',
sourceExerciseId: 'exercise-steps',
position: 0,
exerciseNameSnapshot: 'Circuit tirs',
exerciseStepsSnapshot: steps,
availableTimeSnapshot: false,
availableRepsSnapshot: true,
availableScoreSnapshot: false,
setsCount: 3,
timeEnabled: false,
repsEnabled: true,
scoreEnabled: false,
targetReps: 2,
),
],
),
);
final exerciseRepository = _FakeExerciseRepository();
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
programRepository,
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
program: programRepository.programs.single,
),
),
);
await tester.tap(find.text('Enregistrer'));
await tester.pumpAndSettle();
final savedSteps = programRepository
.programs
.single
.exercises
.single
.exerciseStepsSnapshot;
expect(savedSteps, hasLength(2));
expect(savedSteps.map((step) => step.name), ['Course', 'Tirs']);
expect(savedSteps.map((step) => step.position), [0, 1]);
},
);
testWidgets(
'personnaliser lenchaînement automatique transmet loverride 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 lexercice remet loverride 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 lobjectif de chrono pour un score en mode chrono', (
tester,
) async {
final exerciseRepository = _FakeExerciseRepository()
..exercises.add(
Exercise(
metadata: _metadata('exercise-1'),
name: 'Suicides',
hasTimeMeasure: true,
hasRepsMeasure: true,
hasScoreMeasure: true,
scoreInputMode: ScoreInputMode.stopwatch,
scoreLabel: 'Temps réalisé',
defaultTargetTimeSeconds: 30,
defaultTargetReps: 5,
defaultTargetScoreTimeMs: 12000,
),
);
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
_FakeProgramRepository(),
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
),
),
);
await tester.tap(find.text('Ajouter un exercice'));
await tester.pumpAndSettle();
await tester.tap(find.text('Suicides'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip("Personnaliser l'exercice"));
await tester.pumpAndSettle();
expect(find.text('Score chrono'), findsOneWidget);
await tester.dragUntilVisible(
find.text(
"Temps sert d'objectif de durée ; Score chrono enregistre "
'le temps réalisé.',
),
find.byType(ListView),
const Offset(0, -200),
);
expect(
find.text(
"Temps sert d'objectif de durée ; Score chrono enregistre "
'le temps réalisé.',
),
findsOneWidget,
);
await tester.dragUntilVisible(
find.text('Objectif de chrono'),
find.byType(ListView),
const Offset(0, -200),
);
expect(
find.widgetWithText(TextFormField, 'Objectif de chrono'),
findsOneWidget,
);
expect(find.widgetWithText(TextFormField, 'Cible score'), findsNothing);
expect(
find.text('Le résultat réel sera mesuré pendant la série.'),
findsOneWidget,
);
});
testWidgets(
'supprimer un programme demande confirmation puis le retire de la liste',
(tester) async {
final programRepository = _FakeProgramRepository()
..programs.add(
Program(
metadata: _metadata('program-1'),
name: 'Jambes',
defaultRestSeconds: 60,
),
);
final templateRepository = _FakeWorkoutTemplateRepository()
..templates.add(
WorkoutTemplate(
metadata: _metadata('template-1'),
name: 'Séance jambes',
programs: [
WorkoutTemplateProgram(
metadata: _metadata('template-program-1'),
workoutTemplateId: 'template-1',
sourceProgramId: 'program-1',
position: 0,
programNameSnapshot: 'Jambes',
defaultRestSecondsSnapshot: 60,
programSnapshotJson: '{"exercises":[]}',
),
],
),
);
final exerciseRepository = _FakeExerciseRepository();
await tester.pumpWidget(
MaterialApp(
home: ProgramListScreen(
programUseCases: _programUseCases(
programRepository,
exerciseRepository,
templateRepository: templateRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
),
),
);
await tester.pump();
await tester.pump();
await tester.tap(find.byTooltip('Supprimer le programme'));
await tester.pumpAndSettle();
expect(find.text('Supprimer Jambes ?'), findsOneWidget);
expect(
find.textContaining('Le supprimer le retirera de ces séances.'),
findsOneWidget,
);
await tester.tap(find.text('Annuler'));
await tester.pumpAndSettle();
expect(find.text('Jambes'), findsOneWidget);
expect(programRepository.saved, isEmpty);
await tester.tap(find.byTooltip('Supprimer le programme'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, 'Supprimer'));
await tester.pumpAndSettle();
expect(find.text('Jambes'), findsNothing);
expect(programRepository.saved.single.metadata.deletedAt, isNotNull);
},
);
testWidgets('partager un programme envoie le partage avec un email valide', (
tester,
) async {
final programRepository = _FakeProgramRepository()
..programs.add(
Program(
metadata: _metadata('program-1'),
name: 'Programme tirs',
defaultRestSeconds: 45,
exercises: [
ProgramExercise(
metadata: _metadata('program-exercise-1'),
programId: 'program-1',
sourceExerciseId: 'exercise-1',
position: 0,
exerciseNameSnapshot: 'Tirs',
availableTimeSnapshot: false,
availableRepsSnapshot: true,
availableScoreSnapshot: false,
setsCount: 3,
timeEnabled: false,
repsEnabled: true,
scoreEnabled: false,
targetReps: 10,
),
],
),
);
final exerciseRepository = _FakeExerciseRepository();
final remoteShareApi = _FakeRemoteShareApi();
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
programRepository,
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
shareUseCases: _shareUseCases(
programRepository: programRepository,
remoteShareApi: remoteShareApi,
),
program: programRepository.programs.single,
),
),
);
await tester.tap(find.byTooltip('Partager'));
await tester.pumpAndSettle();
expect(find.text('Partager le programme'), findsOneWidget);
await tester.enterText(
find.widgetWithText(TextFormField, 'Emails destinataires'),
'joueur@example.com',
);
await tester.tap(find.text('Envoyer le partage'));
await tester.pump();
await tester.pump();
expect(remoteShareApi.sendCalls, 1);
expect(remoteShareApi.lastResourceType, ShareResourceType.program);
expect(remoteShareApi.lastRecipientEmails, ['joueur@example.com']);
expect(find.text('Partage envoyé.'), findsOneWidget);
});
testWidgets('partager sans compte affiche lécran compte requis', (
tester,
) async {
final programRepository = _FakeProgramRepository()
..programs.add(
Program(
metadata: _metadata('program-1'),
name: 'Programme tirs',
defaultRestSeconds: 45,
),
);
final exerciseRepository = _FakeExerciseRepository();
await tester.pumpWidget(
MaterialApp(
home: ProgramFormScreen(
programUseCases: _programUseCases(
programRepository,
exerciseRepository,
),
exerciseUseCases: _exerciseUseCases(exerciseRepository),
shareUseCases: _shareUseCases(
tokenStore: _FakeAuthTokenStore(),
programRepository: programRepository,
),
program: programRepository.programs.single,
),
),
);
await tester.tap(find.byTooltip('Partager'));
await tester.pumpAndSettle();
await tester.enterText(
find.widgetWithText(TextFormField, 'Emails destinataires'),
'joueur@example.com',
);
await tester.tap(find.text('Envoyer le partage'));
await tester.pumpAndSettle();
expect(find.text('Compte requis pour partager'), findsWidgets);
expect(find.text('Annuler'), findsOneWidget);
await tester.tap(find.text('Annuler'));
await tester.pumpAndSettle();
expect(find.text('Partager le programme'), findsOneWidget);
});
}
ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) {
return ExerciseUseCases(
repository: repository,
programRepository: _FakeProgramRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}
ProgramUseCases _programUseCases(
_FakeProgramRepository programRepository,
_FakeExerciseRepository exerciseRepository, {
_FakeWorkoutTemplateRepository? templateRepository,
}) {
return ProgramUseCases(
programRepository: programRepository,
exerciseRepository: exerciseRepository,
templateRepository: templateRepository ?? _FakeWorkoutTemplateRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}
ShareUseCases _shareUseCases({
_FakeAuthTokenStore? tokenStore,
_FakeRemoteShareApi? remoteShareApi,
_FakeProgramRepository? programRepository,
_FakeWorkoutTemplateRepository? templateRepository,
}) {
return ShareUseCases(
tokenStore: tokenStore ?? _FakeAuthTokenStore('token-1'),
remoteShareApi: remoteShareApi ?? _FakeRemoteShareApi(),
inboxRepository: _FakeShareInboxRepository(),
pendingActionRepository: _FakePendingShareActionRepository(),
localChanges: _FakeLocalSyncChangeRepository(),
programRepository: programRepository ?? _FakeProgramRepository(),
templateRepository: templateRepository ?? _FakeWorkoutTemplateRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}
final class _FakeWorkoutTemplateRepository
implements WorkoutTemplateRepository {
final templates = <WorkoutTemplate>[];
@override
Future<WorkoutTemplate?> findById(String id) async => null;
@override
Future<List<WorkoutTemplate>> listActive() async {
return templates
.where((template) => template.metadata.deletedAt == null)
.toList();
}
@override
Future<void> replaceComposition(
WorkoutTemplate template,
DateTime deletedAt,
) async {
templates.removeWhere((item) => item.metadata.id == template.metadata.id);
templates.add(template);
}
@override
Future<void> save(WorkoutTemplate template) async {
templates.removeWhere((item) => item.metadata.id == template.metadata.id);
templates.add(template);
}
@override
Future<void> saveOverride(WorkoutTemplateExerciseOverride override) async {}
@override
Future<void> saveProgram(WorkoutTemplateProgram program) async {}
}
EntityMetadata _metadata(String id) {
return EntityMetadata(
id: id,
createdAt: DateTime.utc(2026, 7, 17),
updatedAt: DateTime.utc(2026, 7, 17),
originDeviceId: 'device-1',
);
}
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);
final DateTime value;
@override
DateTime now() => value;
}
final class _FakeIds implements IdGenerator {
var _next = 0;
@override
String newId() {
_next += 1;
return 'id-$_next';
}
}
final class _FakeExerciseRepository implements ExerciseRepository {
final exercises = <Exercise>[];
final saved = <Exercise>[];
@override
Future<Exercise?> findById(String id) async {
for (final exercise in exercises) {
if (exercise.metadata.id == id) {
return exercise;
}
}
return null;
}
@override
Future<bool> isReferencedByProgram(String id) async => false;
@override
Future<List<Exercise>> listActive() async => exercises;
@override
Future<void> save(Exercise exercise) async {
saved.add(exercise);
}
}
final class _FakeProgramRepository implements ProgramRepository {
final programs = <Program>[];
final saved = <Program>[];
@override
Future<Program?> findById(String id) async {
for (final program in programs) {
if (program.metadata.id == id) {
return program;
}
}
return null;
}
@override
Future<List<Program>> listActive() async {
return programs
.where((program) => program.metadata.deletedAt == null)
.toList();
}
@override
Future<void> replaceExercises(Program program, DateTime deletedAt) async {
programs.removeWhere((item) => item.metadata.id == program.metadata.id);
programs.add(program);
}
@override
Future<void> save(Program program) async {
saved.add(program);
programs.removeWhere((item) => item.metadata.id == program.metadata.id);
programs.add(program);
}
@override
Future<void> saveExercise(ProgramExercise exercise) async {}
}
final class _FakeAuthTokenStore implements AuthTokenStore {
_FakeAuthTokenStore([this.token]);
String? token;
@override
Future<void> clearToken() async {
token = null;
}
@override
Future<String?> readToken() async => token;
@override
Future<void> saveToken(String token, DateTime expiresAt) async {
this.token = token;
}
}
final class _FakeRemoteShareApi implements RemoteShareApi {
var sendCalls = 0;
ShareResourceType? lastResourceType;
List<String> lastRecipientEmails = const [];
@override
Future<RemoteSyncedItem> acceptShare(String shareId, String token) async {
return RemoteSyncedItem(
resourceType: SyncResourceType.program,
clientId: 'program-remote',
serverId: 'program-server',
schemaVersion: 1,
clientUpdatedAt: DateTime.utc(2026, 7, 17),
serverUpdatedAt: DateTime.utc(2026, 7, 17),
deletedAt: null,
payload: const {'name': 'Programme partagé'},
);
}
@override
Future<void> declineShare(String shareId, String token) async {}
@override
Future<List<ShareInboxItem>> fetchInbox(String token) async => const [];
@override
Future<void> revokeShare(String shareId, String token) async {}
@override
Future<RemoteShareSendResult> sendShare({
required ShareResourceType resourceType,
required Map<String, Object?> payload,
required List<String> recipientEmails,
required String token,
}) async {
sendCalls += 1;
lastResourceType = resourceType;
lastRecipientEmails = recipientEmails;
return const RemoteShareSendResult(
shareId: 'share-1',
recipientUserIds: ['recipient-1'],
unresolvedEmails: [],
);
}
}
final class _FakeShareInboxRepository implements ShareInboxRepository {
@override
Future<ShareInboxItem?> findByShareId(String shareId) async => null;
@override
Future<List<ShareInboxItem>> listAll() async => const [];
@override
Future<void> markStatus(
String shareId,
ShareInboxStatus status,
DateTime respondedAt,
) async {}
@override
Future<void> upsert(ShareInboxItem item) async {}
@override
Future<void> upsertAll(List<ShareInboxItem> items) async {}
}
final class _FakePendingShareActionRepository
implements PendingShareActionRepository {
@override
Future<void> add(PendingShareAction action) async {}
@override
Future<List<PendingShareAction>> listPending() async => const [];
@override
Future<void> markFailed(String id, DateTime attemptedAt) async {}
@override
Future<void> markSucceeded(String id, DateTime attemptedAt) async {}
}
final class _FakeLocalSyncChangeRepository
implements LocalSyncChangeRepository {
@override
Future<bool> applyRemoteItem(RemoteSyncedItem item) async => true;
@override
Future<List<PendingSyncChange>> listPendingChanges() async => const [];
@override
Future<void> markChangesSynced(
List<String> changeLogIds,
DateTime syncedAt,
) async {}
}