merge(main): exécution de séance avec module séquence et bips (ticket #60)
Fusionne feature/exercise-steps-60-execution-ui — pub get OK, analyze propre (mêmes infos préexistantes + 2 nouvelles use_build_context_synchronously non bloquantes), 100/100 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#58"
|
issueRef: "#58"
|
||||||
version: 1
|
version: 2
|
||||||
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784481524706
|
updatedAt: 1784483616127
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,15 +2,15 @@
|
|||||||
id: "d18752bf-3e5b-4fb9-935c-064d0d2e837a"
|
id: "d18752bf-3e5b-4fb9-935c-064d0d2e837a"
|
||||||
number: 58
|
number: 58
|
||||||
title: "[DevBackend] Exécution persistante des étapes et résultats par passage"
|
title: "[DevBackend] Exécution persistante des étapes et résultats par passage"
|
||||||
status: "open"
|
status: "qa"
|
||||||
priority: "medium"
|
priority: "medium"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: [{"target":"#54","kind":"relatesTo"},{"target":"#56","kind":"dependsOn"}]
|
links: [{"target":"#54","kind":"relatesTo"},{"target":"#56","kind":"dependsOn"}]
|
||||||
agentRefs: [{"agentId":"10ee045b-1c41-479e-ba03-dceed9edd495","role":"assigned"}]
|
agentRefs: [{"agentId":"10ee045b-1c41-479e-ba03-dceed9edd495","role":"assigned"}]
|
||||||
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784481524706
|
createdAt: 1784481524706
|
||||||
updatedAt: 1784481524706
|
updatedAt: 1784483616127
|
||||||
version: 1
|
version: 2
|
||||||
---
|
---
|
||||||
Ajouter le modèle d'exécution des étapes distinct de `ActiveSetResult`. Créer une table dédiée type `ActiveExerciseStepProgressState` pour session+position de série, passage courant, étape courante, statut `idle|running|stopped`, `startedAt`, `accumulatedMs`, et actions pause/reprise/skip. Créer `ActiveExerciseStepResult` pour résultats par `activeWorkoutSessionId + programIndex + exerciseIndex + setIndex + passageIndex + stepSnapshotId/stepIndex`, statut `completed|skipped`, durée/rep cible snapshot, score manuel ou score chrono d'étape. À la clôture de séance, projeter vers `WorkoutHistoryStepResult` sans mélanger avec `WorkoutHistorySetResult`.
|
Ajouter le modèle d'exécution des étapes distinct de `ActiveSetResult`. Créer une table dédiée type `ActiveExerciseStepProgressState` pour session+position de série, passage courant, étape courante, statut `idle|running|stopped`, `startedAt`, `accumulatedMs`, et actions pause/reprise/skip. Créer `ActiveExerciseStepResult` pour résultats par `activeWorkoutSessionId + programIndex + exerciseIndex + setIndex + passageIndex + stepSnapshotId/stepIndex`, statut `completed|skipped`, durée/rep cible snapshot, score manuel ou score chrono d'étape. À la clôture de séance, projeter vers `WorkoutHistoryStepResult` sans mélanger avec `WorkoutHistorySetResult`.
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#59"
|
issueRef: "#59"
|
||||||
version: 1
|
version: 2
|
||||||
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784481530478
|
updatedAt: 1784483616855
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,15 +2,15 @@
|
|||||||
id: "be486426-a17d-4521-b475-9230a2e30169"
|
id: "be486426-a17d-4521-b475-9230a2e30169"
|
||||||
number: 59
|
number: 59
|
||||||
title: "[DevFrontend] Éditeur d'exercice avec séquence d'étapes"
|
title: "[DevFrontend] Éditeur d'exercice avec séquence d'étapes"
|
||||||
status: "open"
|
status: "qa"
|
||||||
priority: "medium"
|
priority: "medium"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: [{"target":"#54","kind":"relatesTo"},{"target":"#56","kind":"dependsOn"}]
|
links: [{"target":"#54","kind":"relatesTo"},{"target":"#56","kind":"dependsOn"}]
|
||||||
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
||||||
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784481530478
|
createdAt: 1784481530478
|
||||||
updatedAt: 1784481530478
|
updatedAt: 1784483616855
|
||||||
version: 1
|
version: 2
|
||||||
---
|
---
|
||||||
Implémenter la section UX `Séquence d'étapes` dans `ExerciseFormScreen` : activation optionnelle, ajout/réordonnancement max 8, écran de détail d'étape, type exclusif temps/répétitions, cible obligatoire > 0, score d'étape optionnel avec mode manual/stopwatch. Préserver l'avertissement existant sur les exercices déjà utilisés : les programmes existants gardent leurs snapshots.
|
Implémenter la section UX `Séquence d'étapes` dans `ExerciseFormScreen` : activation optionnelle, ajout/réordonnancement max 8, écran de détail d'étape, type exclusif temps/répétitions, cible obligatoire > 0, score d'étape optionnel avec mode manual/stopwatch. Préserver l'avertissement existant sur les exercices déjà utilisés : les programmes existants gardent leurs snapshots.
|
||||||
@ -643,25 +643,25 @@
|
|||||||
"issueRef": "#58",
|
"issueRef": "#58",
|
||||||
"path": "58",
|
"path": "58",
|
||||||
"title": "[DevBackend] Exécution persistante des étapes et résultats par passage",
|
"title": "[DevBackend] Exécution persistante des étapes et résultats par passage",
|
||||||
"status": "open",
|
"status": "qa",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"10ee045b-1c41-479e-ba03-dceed9edd495"
|
"10ee045b-1c41-479e-ba03-dceed9edd495"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784481524706
|
"updatedAt": 1784483616127
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#59",
|
"issueRef": "#59",
|
||||||
"path": "59",
|
"path": "59",
|
||||||
"title": "[DevFrontend] Éditeur d'exercice avec séquence d'étapes",
|
"title": "[DevFrontend] Éditeur d'exercice avec séquence d'étapes",
|
||||||
"status": "open",
|
"status": "qa",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784481530478
|
"updatedAt": 1784483616855
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#60",
|
"issueRef": "#60",
|
||||||
|
|||||||
BIN
assets/audio/step_completion_long.wav
Normal file
BIN
assets/audio/step_completion_long.wav
Normal file
Binary file not shown.
BIN
assets/audio/step_countdown_short.wav
Normal file
BIN
assets/audio/step_countdown_short.wav
Normal file
Binary file not shown.
@ -7,6 +7,7 @@ abstract interface class AppDependencies {
|
|||||||
ProgramUseCases get programUseCases;
|
ProgramUseCases get programUseCases;
|
||||||
WorkoutTemplateUseCases get workoutTemplateUseCases;
|
WorkoutTemplateUseCases get workoutTemplateUseCases;
|
||||||
ActiveWorkoutSessionUseCases get activeWorkoutSessionUseCases;
|
ActiveWorkoutSessionUseCases get activeWorkoutSessionUseCases;
|
||||||
|
ActiveExerciseStepUseCases get activeExerciseStepUseCases;
|
||||||
CloseWorkoutSessionUseCase get closeWorkoutSessionUseCase;
|
CloseWorkoutSessionUseCase get closeWorkoutSessionUseCase;
|
||||||
WorkoutHistoryUseCases get workoutHistoryUseCases;
|
WorkoutHistoryUseCases get workoutHistoryUseCases;
|
||||||
}
|
}
|
||||||
@ -19,6 +20,7 @@ final class AppBootstrap implements AppDependencies {
|
|||||||
required this.programUseCases,
|
required this.programUseCases,
|
||||||
required this.workoutTemplateUseCases,
|
required this.workoutTemplateUseCases,
|
||||||
required this.activeWorkoutSessionUseCases,
|
required this.activeWorkoutSessionUseCases,
|
||||||
|
required this.activeExerciseStepUseCases,
|
||||||
required this.closeWorkoutSessionUseCase,
|
required this.closeWorkoutSessionUseCase,
|
||||||
required this.workoutHistoryUseCases,
|
required this.workoutHistoryUseCases,
|
||||||
required this.syncGateway,
|
required this.syncGateway,
|
||||||
@ -36,6 +38,8 @@ final class AppBootstrap implements AppDependencies {
|
|||||||
@override
|
@override
|
||||||
final ActiveWorkoutSessionUseCases activeWorkoutSessionUseCases;
|
final ActiveWorkoutSessionUseCases activeWorkoutSessionUseCases;
|
||||||
@override
|
@override
|
||||||
|
final ActiveExerciseStepUseCases activeExerciseStepUseCases;
|
||||||
|
@override
|
||||||
final CloseWorkoutSessionUseCase closeWorkoutSessionUseCase;
|
final CloseWorkoutSessionUseCase closeWorkoutSessionUseCase;
|
||||||
@override
|
@override
|
||||||
final WorkoutHistoryUseCases workoutHistoryUseCases;
|
final WorkoutHistoryUseCases workoutHistoryUseCases;
|
||||||
@ -93,6 +97,12 @@ final class AppBootstrap implements AppDependencies {
|
|||||||
ids: ids,
|
ids: ids,
|
||||||
originDeviceId: originDeviceId,
|
originDeviceId: originDeviceId,
|
||||||
),
|
),
|
||||||
|
activeExerciseStepUseCases: ActiveExerciseStepUseCases(
|
||||||
|
sessionRepository: activeSessionRepository,
|
||||||
|
clock: clock,
|
||||||
|
ids: ids,
|
||||||
|
originDeviceId: originDeviceId,
|
||||||
|
),
|
||||||
closeWorkoutSessionUseCase: CloseWorkoutSessionUseCase(
|
closeWorkoutSessionUseCase: CloseWorkoutSessionUseCase(
|
||||||
sessionRepository: activeSessionRepository,
|
sessionRepository: activeSessionRepository,
|
||||||
historyRepository: historyRepository,
|
historyRepository: historyRepository,
|
||||||
|
|||||||
49
lib/presentation/exercise_step_audio.dart
Normal file
49
lib/presentation/exercise_step_audio.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
|
|
||||||
|
abstract interface class ExerciseStepAudioCuePlayer {
|
||||||
|
Future<void> playShortCountdownBeep();
|
||||||
|
|
||||||
|
Future<void> playLongCompletionBeep();
|
||||||
|
|
||||||
|
Future<void> dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class AudioplayersExerciseStepAudioCuePlayer
|
||||||
|
implements ExerciseStepAudioCuePlayer {
|
||||||
|
AudioplayersExerciseStepAudioCuePlayer();
|
||||||
|
|
||||||
|
final AudioPlayer _player = AudioPlayer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> playShortCountdownBeep() {
|
||||||
|
return _play('audio/step_countdown_short.wav');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> playLongCompletionBeep() {
|
||||||
|
return _play('audio/step_completion_long.wav');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _play(String assetPath) async {
|
||||||
|
await _player.stop();
|
||||||
|
await _player.play(AssetSource(assetPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() {
|
||||||
|
return _player.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class NoOpExerciseStepAudioCuePlayer implements ExerciseStepAudioCuePlayer {
|
||||||
|
const NoOpExerciseStepAudioCuePlayer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> playShortCountdownBeep() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> playLongCompletionBeep() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {}
|
||||||
|
}
|
||||||
@ -14,12 +14,14 @@ final class HistoryListScreen extends StatefulWidget {
|
|||||||
required this.activeUseCases,
|
required this.activeUseCases,
|
||||||
required this.closeUseCase,
|
required this.closeUseCase,
|
||||||
this.mediaUseCases,
|
this.mediaUseCases,
|
||||||
|
this.stepUseCases,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final WorkoutHistoryUseCases historyUseCases;
|
final WorkoutHistoryUseCases historyUseCases;
|
||||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
|
final ActiveExerciseStepUseCases? stepUseCases;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final MediaUseCases? mediaUseCases;
|
final MediaUseCases? mediaUseCases;
|
||||||
|
|
||||||
@ -94,6 +96,7 @@ final class _HistoryListScreenState extends State<HistoryListScreen> {
|
|||||||
historyUseCases: widget.historyUseCases,
|
historyUseCases: widget.historyUseCases,
|
||||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||||
activeUseCases: widget.activeUseCases,
|
activeUseCases: widget.activeUseCases,
|
||||||
|
stepUseCases: widget.stepUseCases,
|
||||||
closeUseCase: widget.closeUseCase,
|
closeUseCase: widget.closeUseCase,
|
||||||
mediaUseCases: widget.mediaUseCases,
|
mediaUseCases: widget.mediaUseCases,
|
||||||
),
|
),
|
||||||
@ -115,6 +118,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
required this.activeUseCases,
|
required this.activeUseCases,
|
||||||
required this.closeUseCase,
|
required this.closeUseCase,
|
||||||
this.mediaUseCases,
|
this.mediaUseCases,
|
||||||
|
this.stepUseCases,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -122,6 +126,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
final WorkoutHistoryUseCases historyUseCases;
|
final WorkoutHistoryUseCases historyUseCases;
|
||||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
|
final ActiveExerciseStepUseCases? stepUseCases;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final MediaUseCases? mediaUseCases;
|
final MediaUseCases? mediaUseCases;
|
||||||
|
|
||||||
@ -193,6 +198,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
builder: (context) => WorkoutExecutionScreen(
|
builder: (context) => WorkoutExecutionScreen(
|
||||||
initialSession: session,
|
initialSession: session,
|
||||||
activeUseCases: activeUseCases,
|
activeUseCases: activeUseCases,
|
||||||
|
stepUseCases: stepUseCases,
|
||||||
closeUseCase: closeUseCase,
|
closeUseCase: closeUseCase,
|
||||||
historyUseCases: historyUseCases,
|
historyUseCases: historyUseCases,
|
||||||
workoutTemplateUseCases: workoutTemplateUseCases,
|
workoutTemplateUseCases: workoutTemplateUseCases,
|
||||||
|
|||||||
@ -152,6 +152,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
widget.bootstrap.workoutTemplateUseCases,
|
widget.bootstrap.workoutTemplateUseCases,
|
||||||
programUseCases: widget.bootstrap.programUseCases,
|
programUseCases: widget.bootstrap.programUseCases,
|
||||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||||
|
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||||
@ -170,6 +171,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
workoutTemplateUseCases:
|
workoutTemplateUseCases:
|
||||||
widget.bootstrap.workoutTemplateUseCases,
|
widget.bootstrap.workoutTemplateUseCases,
|
||||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||||
|
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||||
),
|
),
|
||||||
@ -187,6 +189,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
builder: (context) => WorkoutExecutionScreen(
|
builder: (context) => WorkoutExecutionScreen(
|
||||||
initialSession: session,
|
initialSession: session,
|
||||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||||
|
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||||
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export 'game_time_app.dart';
|
export 'game_time_app.dart';
|
||||||
|
export 'exercise_step_audio.dart';
|
||||||
export 'exercise_library_screen.dart';
|
export 'exercise_library_screen.dart';
|
||||||
export 'history_screen.dart';
|
export 'history_screen.dart';
|
||||||
export 'home_screen.dart';
|
export 'home_screen.dart';
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -14,12 +14,14 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
|||||||
required this.closeUseCase,
|
required this.closeUseCase,
|
||||||
required this.historyUseCases,
|
required this.historyUseCases,
|
||||||
this.mediaUseCases,
|
this.mediaUseCases,
|
||||||
|
this.stepUseCases,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||||
final ProgramUseCases programUseCases;
|
final ProgramUseCases programUseCases;
|
||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
|
final ActiveExerciseStepUseCases? stepUseCases;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final WorkoutHistoryUseCases historyUseCases;
|
final WorkoutHistoryUseCases historyUseCases;
|
||||||
final MediaUseCases? mediaUseCases;
|
final MediaUseCases? mediaUseCases;
|
||||||
@ -140,6 +142,7 @@ final class _WorkoutTemplateListScreenState
|
|||||||
builder: (context) => WorkoutExecutionScreen(
|
builder: (context) => WorkoutExecutionScreen(
|
||||||
initialSession: session,
|
initialSession: session,
|
||||||
activeUseCases: widget.activeUseCases,
|
activeUseCases: widget.activeUseCases,
|
||||||
|
stepUseCases: widget.stepUseCases,
|
||||||
closeUseCase: widget.closeUseCase,
|
closeUseCase: widget.closeUseCase,
|
||||||
historyUseCases: widget.historyUseCases,
|
historyUseCases: widget.historyUseCases,
|
||||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||||
|
|||||||
72
pubspec.lock
72
pubspec.lock
@ -41,6 +41,62 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.1"
|
version: "2.13.1"
|
||||||
|
audioplayers:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: audioplayers
|
||||||
|
sha256: "2ba4bb2944baacbdd5372ff8254a8e7feb8c10d7739545e392f5605a8f618745"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.8.1"
|
||||||
|
audioplayers_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_android
|
||||||
|
sha256: f5ff5b15620fbab8cb0849e9636c48e2b96c3f0f71723bbbe2ad3c761b205f05
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.3.0"
|
||||||
|
audioplayers_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_darwin
|
||||||
|
sha256: "1ca553add991384ecf421b9569da850f3ab2472ffb83f6970b0416365abc51be"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
|
audioplayers_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_linux
|
||||||
|
sha256: "15178b726b7cdee5364d0463c8d445630c4e0fb7d26612b73c767e7d25de9417"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
|
audioplayers_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_platform_interface
|
||||||
|
sha256: "765f6f0e6dca55cb471c9483fc77700564b3484d19198aca4ebb5147c6c85acb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.2.0"
|
||||||
|
audioplayers_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_web
|
||||||
|
sha256: ae1e0103c865a03e273f6d13d97b93f5595eac09915729cd5e37ef96e2857319
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.3.0"
|
||||||
|
audioplayers_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audioplayers_windows
|
||||||
|
sha256: a70ae82bba2dfcb6eb03dd4815d737a2d46d33ea5a96a03f535cfcaac490e413
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.4.1"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -813,6 +869,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -837,6 +901,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.6.0"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -14,6 +14,7 @@ dependencies:
|
|||||||
drift: ^2.34.2
|
drift: ^2.34.2
|
||||||
drift_flutter: ^0.3.1
|
drift_flutter: ^0.3.1
|
||||||
image_picker: ^1.2.3
|
image_picker: ^1.2.3
|
||||||
|
audioplayers: ^6.8.1
|
||||||
path: ^1.9.1
|
path: ^1.9.1
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
sqlite3_flutter_libs: ^0.6.0+eol
|
sqlite3_flutter_libs: ^0.6.0+eol
|
||||||
@ -29,6 +30,8 @@ dev_dependencies:
|
|||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
|
- assets/audio/
|
||||||
fonts:
|
fonts:
|
||||||
- family: Anton
|
- family: Anton
|
||||||
fonts:
|
fonts:
|
||||||
|
|||||||
@ -122,6 +122,12 @@ final class _FakeBootstrap implements AppDependencies {
|
|||||||
ids: _FakeIds(),
|
ids: _FakeIds(),
|
||||||
originDeviceId: 'device-1',
|
originDeviceId: 'device-1',
|
||||||
),
|
),
|
||||||
|
activeExerciseStepUseCases = ActiveExerciseStepUseCases(
|
||||||
|
sessionRepository: activeRepository,
|
||||||
|
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
),
|
||||||
closeWorkoutSessionUseCase = CloseWorkoutSessionUseCase(
|
closeWorkoutSessionUseCase = CloseWorkoutSessionUseCase(
|
||||||
sessionRepository: activeRepository,
|
sessionRepository: activeRepository,
|
||||||
historyRepository: _FakeWorkoutHistoryRepository(),
|
historyRepository: _FakeWorkoutHistoryRepository(),
|
||||||
@ -179,6 +185,9 @@ final class _FakeBootstrap implements AppDependencies {
|
|||||||
@override
|
@override
|
||||||
final ActiveWorkoutSessionUseCases activeWorkoutSessionUseCases;
|
final ActiveWorkoutSessionUseCases activeWorkoutSessionUseCases;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final ActiveExerciseStepUseCases activeExerciseStepUseCases;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final CloseWorkoutSessionUseCase closeWorkoutSessionUseCase;
|
final CloseWorkoutSessionUseCase closeWorkoutSessionUseCase;
|
||||||
|
|
||||||
|
|||||||
@ -942,6 +942,379 @@ void main() {
|
|||||||
expect(find.text('2 / 3'), findsOneWidget);
|
expect(find.text('2 / 3'), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('le module séquence s’affiche pour un exercice avec étapes', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 1600));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final repository = _FakeActiveSessionRepository();
|
||||||
|
final session = ActiveWorkoutSession(
|
||||||
|
metadata: _metadata('session-1'),
|
||||||
|
sourceWorkoutTemplateId: 'template-1',
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
elapsedActiveMs: 0,
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
|
targetReps: 10,
|
||||||
|
exerciseSteps: [
|
||||||
|
_step('step-1', 0, 'Dribble main droite', ExerciseStepType.time, 10),
|
||||||
|
_step('step-2', 1, 'Pompes', ExerciseStepType.reps, 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
repository.session = session;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: WorkoutExecutionScreen(
|
||||||
|
initialSession: session,
|
||||||
|
activeUseCases: _activeUseCases(repository, clock),
|
||||||
|
stepUseCases: _stepUseCases(repository, clock),
|
||||||
|
stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
|
||||||
|
closeUseCase: _closeUseCase(repository, clock),
|
||||||
|
historyUseCases: _historyUseCases(clock),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('SÉQUENCE'), findsOneWidget);
|
||||||
|
expect(find.text('Passage 1 / 10'), findsOneWidget);
|
||||||
|
expect(find.text('ÉTAPE 1 / 2'), findsOneWidget);
|
||||||
|
expect(find.text('Dribble main droite'), findsOneWidget);
|
||||||
|
expect(find.text('00:10'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('une étape répétitions peut être validée', (tester) async {
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 1600));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final repository = _FakeActiveSessionRepository();
|
||||||
|
final session = ActiveWorkoutSession(
|
||||||
|
metadata: _metadata('session-1'),
|
||||||
|
sourceWorkoutTemplateId: 'template-1',
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
elapsedActiveMs: 0,
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
|
targetReps: 1,
|
||||||
|
exerciseSteps: [
|
||||||
|
_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
repository.session = session;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: WorkoutExecutionScreen(
|
||||||
|
initialSession: session,
|
||||||
|
activeUseCases: _activeUseCases(repository, clock),
|
||||||
|
stepUseCases: _stepUseCases(repository, clock),
|
||||||
|
stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
|
||||||
|
closeUseCase: _closeUseCase(repository, clock),
|
||||||
|
historyUseCases: _historyUseCases(clock),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('8'), findsWidgets);
|
||||||
|
await tester.tap(find.text('Étape suivante'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(repository.stepResults.single.actualReps, 8);
|
||||||
|
expect(find.text('Séquence terminée'), findsWidgets);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('une étape temps affiche un décompte recalculé', (tester) async {
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 1600));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final repository = _FakeActiveSessionRepository();
|
||||||
|
final audio = _FakeStepAudioCuePlayer();
|
||||||
|
final session = ActiveWorkoutSession(
|
||||||
|
metadata: _metadata('session-1'),
|
||||||
|
sourceWorkoutTemplateId: 'template-1',
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
elapsedActiveMs: 0,
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
|
targetReps: 1,
|
||||||
|
exerciseSteps: [
|
||||||
|
_step('step-1', 0, 'Gainage', ExerciseStepType.time, 5),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
repository.session = session;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: WorkoutExecutionScreen(
|
||||||
|
initialSession: session,
|
||||||
|
activeUseCases: _activeUseCases(repository, clock),
|
||||||
|
stepUseCases: _stepUseCases(repository, clock),
|
||||||
|
stepAudioCuePlayer: audio,
|
||||||
|
closeUseCase: _closeUseCase(repository, clock),
|
||||||
|
historyUseCases: _historyUseCases(clock),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('00:05'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('Démarrer la séquence'));
|
||||||
|
await tester.pump();
|
||||||
|
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 2);
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(find.text('00:03'), findsOneWidget);
|
||||||
|
expect(audio.shortBeeps, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('deux étapes temps consécutives s’enchaînent automatiquement', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 1600));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final repository = _FakeActiveSessionRepository();
|
||||||
|
final audio = _FakeStepAudioCuePlayer();
|
||||||
|
final session = ActiveWorkoutSession(
|
||||||
|
metadata: _metadata('session-1'),
|
||||||
|
sourceWorkoutTemplateId: 'template-1',
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
elapsedActiveMs: 0,
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
|
targetReps: 1,
|
||||||
|
exerciseSteps: [
|
||||||
|
_step('step-1', 0, 'Gauche', ExerciseStepType.time, 1),
|
||||||
|
_step('step-2', 1, 'Droite', ExerciseStepType.time, 2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
repository.session = session;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: WorkoutExecutionScreen(
|
||||||
|
initialSession: session,
|
||||||
|
activeUseCases: _activeUseCases(repository, clock),
|
||||||
|
stepUseCases: _stepUseCases(repository, clock),
|
||||||
|
stepAudioCuePlayer: audio,
|
||||||
|
closeUseCase: _closeUseCase(repository, clock),
|
||||||
|
historyUseCases: _historyUseCases(clock),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Démarrer la séquence'));
|
||||||
|
await tester.pump();
|
||||||
|
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1);
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(repository.stepResults.single.stepIndex, 0);
|
||||||
|
expect(repository.stepResults.single.status, SetResultStatus.completed);
|
||||||
|
expect(find.text('ÉTAPE 2 / 2'), findsOneWidget);
|
||||||
|
expect(find.text('Droite'), findsOneWidget);
|
||||||
|
expect(find.text('00:02'), findsOneWidget);
|
||||||
|
expect(audio.longBeeps, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('la séquence passe au deuxième passage si répétitions > 1', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 1600));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final repository = _FakeActiveSessionRepository();
|
||||||
|
final session = ActiveWorkoutSession(
|
||||||
|
metadata: _metadata('session-1'),
|
||||||
|
sourceWorkoutTemplateId: 'template-1',
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
elapsedActiveMs: 0,
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
|
targetReps: 2,
|
||||||
|
exerciseSteps: [
|
||||||
|
_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
repository.session = session;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: WorkoutExecutionScreen(
|
||||||
|
initialSession: session,
|
||||||
|
activeUseCases: _activeUseCases(repository, clock),
|
||||||
|
stepUseCases: _stepUseCases(repository, clock),
|
||||||
|
stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
|
||||||
|
closeUseCase: _closeUseCase(repository, clock),
|
||||||
|
historyUseCases: _historyUseCases(clock),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.text('Étape suivante'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Passage 2 / 2'), findsOneWidget);
|
||||||
|
expect(find.text('Passages réalisés : 1 / 2'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('terminer la série reste utilisable avant la fin de séquence', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 1800));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final repository = _FakeActiveSessionRepository();
|
||||||
|
final session = ActiveWorkoutSession(
|
||||||
|
metadata: _metadata('session-1'),
|
||||||
|
sourceWorkoutTemplateId: 'template-1',
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
elapsedActiveMs: 0,
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
|
setsCount: 2,
|
||||||
|
restSeconds: 0,
|
||||||
|
targetReps: 2,
|
||||||
|
exerciseSteps: [
|
||||||
|
_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
repository.session = session;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: WorkoutExecutionScreen(
|
||||||
|
initialSession: session,
|
||||||
|
activeUseCases: _activeUseCases(repository, clock),
|
||||||
|
stepUseCases: _stepUseCases(repository, clock),
|
||||||
|
stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
|
||||||
|
closeUseCase: _closeUseCase(repository, clock),
|
||||||
|
historyUseCases: _historyUseCases(clock),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.ensureVisible(find.text('Terminer la série'));
|
||||||
|
await tester.tap(find.text('Terminer la série'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(repository.results.single.setIndex, 0);
|
||||||
|
expect(repository.session?.currentSetIndex, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('les trois niveaux de skip de séquence fonctionnent', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 1800));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final repository = _FakeActiveSessionRepository();
|
||||||
|
final session = ActiveWorkoutSession(
|
||||||
|
metadata: _metadata('session-1'),
|
||||||
|
sourceWorkoutTemplateId: 'template-1',
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
|
||||||
|
elapsedActiveMs: 0,
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
|
targetReps: 2,
|
||||||
|
exerciseSteps: [
|
||||||
|
_step('step-1', 0, 'Gauche', ExerciseStepType.reps, 3),
|
||||||
|
_step('step-2', 1, 'Droite', ExerciseStepType.reps, 3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
repository.session = session;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: WorkoutExecutionScreen(
|
||||||
|
initialSession: session,
|
||||||
|
activeUseCases: _activeUseCases(repository, clock),
|
||||||
|
stepUseCases: _stepUseCases(repository, clock),
|
||||||
|
stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
|
||||||
|
closeUseCase: _closeUseCase(repository, clock),
|
||||||
|
historyUseCases: _historyUseCases(clock),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Passer l’étape'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.widgetWithText(FilledButton, 'Passer l’étape'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(repository.stepResults.single.stepIndex, 0);
|
||||||
|
expect(repository.stepResults.single.status, SetResultStatus.skipped);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Passer ce passage'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.widgetWithText(FilledButton, 'Passer ce passage'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Passage 2 / 2'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Passer la séquence'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.widgetWithText(FilledButton, 'Passer la séquence'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Séquence terminée'), findsWidgets);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Finder _textFieldWithLabel(String label) {
|
Finder _textFieldWithLabel(String label) {
|
||||||
@ -987,6 +1360,18 @@ CloseWorkoutSessionUseCase _closeUseCase(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ActiveExerciseStepUseCases _stepUseCases(
|
||||||
|
_FakeActiveSessionRepository repository,
|
||||||
|
_FakeClock clock,
|
||||||
|
) {
|
||||||
|
return ActiveExerciseStepUseCases(
|
||||||
|
sessionRepository: repository,
|
||||||
|
clock: clock,
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
WorkoutHistoryUseCases _historyUseCases(_FakeClock clock) {
|
WorkoutHistoryUseCases _historyUseCases(_FakeClock clock) {
|
||||||
return WorkoutHistoryUseCases(
|
return WorkoutHistoryUseCases(
|
||||||
repository: _FakeWorkoutHistoryRepository(),
|
repository: _FakeWorkoutHistoryRepository(),
|
||||||
@ -1014,6 +1399,7 @@ String _sessionSnapshot({
|
|||||||
String scoreUnit = 'kg',
|
String scoreUnit = 'kg',
|
||||||
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
||||||
List<String> imageMediaIds = const [],
|
List<String> imageMediaIds = const [],
|
||||||
|
List<Map<String, Object?>> exerciseSteps = const [],
|
||||||
String? videoMediaId,
|
String? videoMediaId,
|
||||||
}) {
|
}) {
|
||||||
return jsonEncode({
|
return jsonEncode({
|
||||||
@ -1043,6 +1429,7 @@ String _sessionSnapshot({
|
|||||||
'imageMediaIdsSnapshot': imageMediaIds,
|
'imageMediaIdsSnapshot': imageMediaIds,
|
||||||
'exerciseVideoMediaIdSnapshot': videoMediaId,
|
'exerciseVideoMediaIdSnapshot': videoMediaId,
|
||||||
'videoMediaIdSnapshot': videoMediaId,
|
'videoMediaIdSnapshot': videoMediaId,
|
||||||
|
'exerciseStepsSnapshot': exerciseSteps,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -1052,6 +1439,34 @@ String _sessionSnapshot({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, Object?> _step(
|
||||||
|
String id,
|
||||||
|
int position,
|
||||||
|
String name,
|
||||||
|
ExerciseStepType type,
|
||||||
|
int defaultTargetValue, {
|
||||||
|
bool hasScore = false,
|
||||||
|
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
||||||
|
String? scoreLabel,
|
||||||
|
String? scoreUnit,
|
||||||
|
double? defaultTargetScore,
|
||||||
|
int? defaultTargetScoreTimeMs,
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'position': position,
|
||||||
|
'name': name,
|
||||||
|
'type': type.name,
|
||||||
|
'defaultTargetValue': defaultTargetValue,
|
||||||
|
'hasScore': hasScore,
|
||||||
|
'scoreInputMode': scoreInputMode.name,
|
||||||
|
'scoreLabel': scoreLabel,
|
||||||
|
'scoreUnit': scoreUnit,
|
||||||
|
'defaultTargetScore': defaultTargetScore,
|
||||||
|
'defaultTargetScoreTimeMs': defaultTargetScoreTimeMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
EntityMetadata _metadata(String id) {
|
EntityMetadata _metadata(String id) {
|
||||||
return EntityMetadata(
|
return EntityMetadata(
|
||||||
id: id,
|
id: id,
|
||||||
@ -1080,6 +1495,27 @@ final class _FakeIds implements IdGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class _FakeStepAudioCuePlayer implements ExerciseStepAudioCuePlayer {
|
||||||
|
var shortBeeps = 0;
|
||||||
|
var longBeeps = 0;
|
||||||
|
var disposed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> playShortCountdownBeep() async {
|
||||||
|
shortBeeps += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> playLongCompletionBeep() async {
|
||||||
|
longBeeps += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||||||
ActiveWorkoutSession? session;
|
ActiveWorkoutSession? session;
|
||||||
final results = <ActiveSetResult>[];
|
final results = <ActiveSetResult>[];
|
||||||
|
|||||||
Reference in New Issue
Block a user