feat(exercice): exécution de séance avec module séquence et bips (ticket #60)
Ajoute le lecteur de bips d'étapes (presentation/exercise_step_audio.dart, port ExerciseStepAudioCuePlayer injectable en test, jamais le vrai audioplayers en test) et les fichiers audio associés (assets/audio/step_countdown_short.wav, step_completion_long.wav). Câble ActiveExerciseStepUseCases dans app_bootstrap.dart et l'expose à WorkoutExecutionScreen. Les appels audio réels sont protégés par un try/catch qui log sans jamais bloquer la progression d'étape. flutter pub get OK (ajout audioplayers), 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:
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