Sépare la notion de Durée (paramètre configuré) de Temps réalisé (résultat mesuré au chronomètre), renomme le champ « Temps par défaut (s) » en « Durée par défaut (s) » dans la bibliothèque d'exercices, et limite l'exécution à un seul chrono visible à la fois. Le mode Score chrono est conservé tel quel comme mode du score. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1219 lines
35 KiB
Dart
1219 lines
35 KiB
Dart
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('Actions'), 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('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 {
|
||
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 l’exercice 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 l’enchaînement automatique transmet l’override 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 l’exercice remet l’override 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 l’objectif 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 chrono (optionnel)'),
|
||
find.byType(ListView),
|
||
const Offset(0, -200),
|
||
);
|
||
expect(
|
||
find.widgetWithText(TextFormField, 'Objectif chrono (optionnel)'),
|
||
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('Actions'));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.text('Supprimer'));
|
||
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('Actions'));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.text('Supprimer'));
|
||
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 {}
|
||
}
|