feat(watch): clôture lot #91 - fréquence cardiaque live, notifications de séance et finitions montre
Consolide le lot applicatif watch companion validé : - télémétrie fréquence cardiaque live remontée montre -> téléphone (collecteur watch, adapter Wear Data Layer, persistance Drift, propagation aux écrans historique/programme/profil/exécution) - notifications de séance en arrière-plan côté téléphone (service foreground de statut + passerelle applicative) - finitions montre : chrono d'étape, score d'étape, retrait du bouton "lancer une séance", thème, icônes et polices watch_app Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -405,6 +405,66 @@ void main() {
|
||||
expect(find.text('Mode de score'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('une étape peut utiliser le score de la série', (tester) async {
|
||||
final exerciseRepository = _FakeExerciseRepository();
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(400, 4200));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
|
||||
await _pumpExerciseForm(tester, exerciseRepository);
|
||||
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Tirs');
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
||||
'30',
|
||||
);
|
||||
await tester.tap(find.widgetWithText(SwitchListTile, 'Score'));
|
||||
await tester.pump();
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Score à saisir').first,
|
||||
'Paniers',
|
||||
);
|
||||
await tester.enterText(find.widgetWithText(TextFormField, 'Unité'), 'pts');
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextFormField, 'Score par défaut'),
|
||||
'0',
|
||||
);
|
||||
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,
|
||||
'Tirs main droite',
|
||||
);
|
||||
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.tap(
|
||||
find.widgetWithText(SwitchListTile, 'Utiliser le score de la série'),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.ensureVisible(find.text('Enregistrer'));
|
||||
await tester.tap(find.text('Enregistrer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final step = exerciseRepository.saved.single.steps.single;
|
||||
expect(step.linkedToSeriesScore, isTrue);
|
||||
expect(step.scoreLabel, 'Paniers');
|
||||
expect(step.scoreUnit, 'pts');
|
||||
expect(step.defaultTargetScore, isNull);
|
||||
});
|
||||
|
||||
testWidgets('réordonner les étapes via Monter Descendre', (tester) async {
|
||||
final exerciseRepository = _FakeExerciseRepository();
|
||||
|
||||
@ -543,6 +603,82 @@ void main() {
|
||||
expect(saved.steps.single.defaultTargetValue, 12);
|
||||
});
|
||||
|
||||
testWidgets('une étape invalide affiche une erreur inline sans sauvegarder', (
|
||||
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,
|
||||
'abc',
|
||||
);
|
||||
|
||||
await tester.ensureVisible(find.text('Enregistrer'));
|
||||
await tester.tap(find.text('Enregistrer'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Saisis une durée supérieure à 0.'), findsOneWidget);
|
||||
expect(exerciseRepository.saved, isEmpty);
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('un échec inattendu de sauvegarde libère le bouton', (
|
||||
tester,
|
||||
) async {
|
||||
final exerciseRepository = _FakeExerciseRepository()..throwOnSave = true;
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(400, 1600));
|
||||
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.ensureVisible(find.text('Enregistrer'));
|
||||
await tester.tap(find.text('Enregistrer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
'Impossible d’enregistrer l’exercice. Vérifie les champs puis réessaie.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<FilledButton>(
|
||||
find.widgetWithText(FilledButton, 'Enregistrer'),
|
||||
)
|
||||
.onPressed,
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'le réglage d’enchaînement automatique des étapes est sauvegardé',
|
||||
(tester) async {
|
||||
@ -839,6 +975,7 @@ final class _FakeExerciseRepository implements ExerciseRepository {
|
||||
final exercises = <Exercise>[];
|
||||
final saved = <Exercise>[];
|
||||
var referenced = false;
|
||||
var throwOnSave = false;
|
||||
|
||||
@override
|
||||
Future<Exercise?> findById(String id) async {
|
||||
@ -862,6 +999,9 @@ final class _FakeExerciseRepository implements ExerciseRepository {
|
||||
|
||||
@override
|
||||
Future<void> save(Exercise exercise) async {
|
||||
if (throwOnSave) {
|
||||
throw Exception('save failed');
|
||||
}
|
||||
saved.add(exercise);
|
||||
exercises.removeWhere((item) => item.metadata.id == exercise.metadata.id);
|
||||
exercises.add(exercise);
|
||||
|
||||
Reference in New Issue
Block a user