3019 lines
98 KiB
Dart
3019 lines
98 KiB
Dart
import 'dart:convert';
|
||
import 'dart:io';
|
||
|
||
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';
|
||
import 'package:watch_bridge_contract/watch_bridge_contract.dart';
|
||
|
||
void main() {
|
||
testWidgets('le temps affiché est recalculé depuis les horodatages', (
|
||
tester,
|
||
) async {
|
||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12, 0, 15));
|
||
final session = ActiveWorkoutSession(
|
||
metadata: _metadata('session-1'),
|
||
sourceWorkoutTemplateId: 'template-1',
|
||
status: ActiveWorkoutStatus.running,
|
||
startedAt: DateTime.utc(2026, 7, 17, 11, 59),
|
||
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
|
||
elapsedActiveMs: 30000,
|
||
currentProgramIndex: 0,
|
||
currentExerciseIndex: 0,
|
||
currentSetIndex: 0,
|
||
resolvedTemplateSnapshotJson: _sessionSnapshot(),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(
|
||
_FakeActiveSessionRepository(),
|
||
clock,
|
||
),
|
||
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Squat'), findsOneWidget);
|
||
expect(find.text('Durée de série : 00:45'), findsOneWidget);
|
||
expect(find.widgetWithText(FilledButton, 'Démarrer'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('une séance invalide est abandonnée avec un message clair', (
|
||
tester,
|
||
) async {
|
||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12, 0, 15));
|
||
final repository = _FakeActiveSessionRepository();
|
||
final session = ActiveWorkoutSession(
|
||
metadata: _metadata('session-1'),
|
||
sourceWorkoutTemplateId: 'template-1',
|
||
status: ActiveWorkoutStatus.running,
|
||
startedAt: DateTime.utc(2026, 7, 17, 11, 59),
|
||
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
|
||
elapsedActiveMs: 30000,
|
||
currentProgramIndex: 0,
|
||
currentExerciseIndex: 0,
|
||
currentSetIndex: 0,
|
||
resolvedTemplateSnapshotJson: _emptySessionSnapshot(),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
expect(find.text('Séance indisponible'), findsOneWidget);
|
||
expect(
|
||
find.text(
|
||
'Cette séance ne peut plus être reprise. Elle a été abandonnée automatiquement.',
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
expect(repository.session?.status, ActiveWorkoutStatus.abandoned);
|
||
});
|
||
|
||
testWidgets('les répétitions sont initialisées avec la cible', (
|
||
tester,
|
||
) async {
|
||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||
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: 8),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(
|
||
_FakeActiveSessionRepository(),
|
||
clock,
|
||
),
|
||
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Répétitions'), findsOneWidget);
|
||
expect(find.text('8'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('affiche les statistiques montre live disponibles', (
|
||
tester,
|
||
) async {
|
||
final now = DateTime.now().toUtc();
|
||
final clock = _FakeClock(now);
|
||
final repository = _FakeActiveSessionRepository();
|
||
final sensorUseCases = ActiveWorkoutSensorUseCases(clock: clock)
|
||
..recordHeartRateSample(
|
||
WatchSensorSample(
|
||
sessionId: 'session-1',
|
||
recordedAtEpochMs: now
|
||
.subtract(const Duration(hours: 1))
|
||
.millisecondsSinceEpoch,
|
||
heartRateBpm: 124,
|
||
),
|
||
)
|
||
..recordHeartRateSample(
|
||
WatchSensorSample(
|
||
sessionId: 'session-1',
|
||
recordedAtEpochMs: now.millisecondsSinceEpoch,
|
||
heartRateBpm: 124,
|
||
distanceMeters: 840,
|
||
caloriesKcal: 186,
|
||
),
|
||
);
|
||
addTearDown(sensorUseCases.dispose);
|
||
final session = ActiveWorkoutSession(
|
||
metadata: _metadata('session-1'),
|
||
sourceWorkoutTemplateId: 'template-1',
|
||
status: ActiveWorkoutStatus.running,
|
||
startedAt: now,
|
||
lastPersistedAt: now,
|
||
elapsedActiveMs: 0,
|
||
currentProgramIndex: 0,
|
||
currentExerciseIndex: 0,
|
||
currentSetIndex: 0,
|
||
resolvedTemplateSnapshotJson: _sessionSnapshot(),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
sensorUseCases: sensorUseCases,
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('FC 124 bpm'), findsOneWidget);
|
||
expect(find.text('840 m'), findsOneWidget);
|
||
expect(find.text('186 kcal'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('n’affiche aucun indicateur capteur sans donnée FC', (
|
||
tester,
|
||
) async {
|
||
final now = DateTime.now().toUtc();
|
||
final clock = _FakeClock(now);
|
||
final repository = _FakeActiveSessionRepository();
|
||
final sensorUseCases = ActiveWorkoutSensorUseCases(clock: clock);
|
||
addTearDown(sensorUseCases.dispose);
|
||
final session = ActiveWorkoutSession(
|
||
metadata: _metadata('session-1'),
|
||
sourceWorkoutTemplateId: 'template-1',
|
||
status: ActiveWorkoutStatus.running,
|
||
startedAt: now,
|
||
lastPersistedAt: now,
|
||
elapsedActiveMs: 0,
|
||
currentProgramIndex: 0,
|
||
currentExerciseIndex: 0,
|
||
currentSetIndex: 0,
|
||
resolvedTemplateSnapshotJson: _sessionSnapshot(),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
sensorUseCases: sensorUseCases,
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.textContaining('bpm'), findsNothing);
|
||
expect(find.textContaining('kcal'), findsNothing);
|
||
expect(find.byIcon(Icons.favorite), findsNothing);
|
||
expect(find.byIcon(Icons.local_fire_department), findsNothing);
|
||
});
|
||
|
||
testWidgets('ne fabrique pas de calories sans mesure montre native', (
|
||
tester,
|
||
) async {
|
||
final now = DateTime.now().toUtc();
|
||
final clock = _FakeClock(now);
|
||
final repository = _FakeActiveSessionRepository();
|
||
final sensorUseCases = ActiveWorkoutSensorUseCases(clock: clock)
|
||
..recordHeartRateSample(
|
||
WatchSensorSample(
|
||
sessionId: 'session-1',
|
||
recordedAtEpochMs: now
|
||
.subtract(const Duration(hours: 1))
|
||
.millisecondsSinceEpoch,
|
||
heartRateBpm: 124,
|
||
),
|
||
)
|
||
..recordHeartRateSample(
|
||
WatchSensorSample(
|
||
sessionId: 'session-1',
|
||
recordedAtEpochMs: now.millisecondsSinceEpoch,
|
||
heartRateBpm: 124,
|
||
),
|
||
);
|
||
addTearDown(sensorUseCases.dispose);
|
||
final session = ActiveWorkoutSession(
|
||
metadata: _metadata('session-1'),
|
||
sourceWorkoutTemplateId: 'template-1',
|
||
status: ActiveWorkoutStatus.running,
|
||
startedAt: now,
|
||
lastPersistedAt: now,
|
||
elapsedActiveMs: 0,
|
||
currentProgramIndex: 0,
|
||
currentExerciseIndex: 0,
|
||
currentSetIndex: 0,
|
||
resolvedTemplateSnapshotJson: _sessionSnapshot(),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
sensorUseCases: sensorUseCases,
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('FC 124 bpm'), findsOneWidget);
|
||
expect(find.textContaining('kcal'), findsNothing);
|
||
});
|
||
|
||
testWidgets('atténue la fréquence cardiaque périmée', (tester) async {
|
||
final now = DateTime.now().toUtc();
|
||
final clock = _FakeClock(now);
|
||
final repository = _FakeActiveSessionRepository();
|
||
final sensorUseCases = ActiveWorkoutSensorUseCases(clock: clock)
|
||
..recordHeartRateSample(
|
||
WatchSensorSample(
|
||
sessionId: 'session-1',
|
||
recordedAtEpochMs: now
|
||
.subtract(const Duration(seconds: 30))
|
||
.millisecondsSinceEpoch,
|
||
heartRateBpm: 118,
|
||
),
|
||
);
|
||
addTearDown(sensorUseCases.dispose);
|
||
final session = ActiveWorkoutSession(
|
||
metadata: _metadata('session-1'),
|
||
sourceWorkoutTemplateId: 'template-1',
|
||
status: ActiveWorkoutStatus.running,
|
||
startedAt: now,
|
||
lastPersistedAt: now,
|
||
elapsedActiveMs: 0,
|
||
currentProgramIndex: 0,
|
||
currentExerciseIndex: 0,
|
||
currentSetIndex: 0,
|
||
resolvedTemplateSnapshotJson: _sessionSnapshot(),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
sensorUseCases: sensorUseCases,
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('FC 118 bpm'), findsOneWidget);
|
||
expect(
|
||
find.ancestor(
|
||
of: find.text('FC 118 bpm'),
|
||
matching: find.byWidgetPredicate(
|
||
(widget) => widget is Opacity && widget.opacity == 0.45,
|
||
),
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
});
|
||
|
||
testWidgets('les références de temps sont formatées sans valeurs brutes', (
|
||
tester,
|
||
) async {
|
||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||
final repository = _FakeActiveSessionRepository();
|
||
final performanceRepository = _FakeExercisePerformanceReferenceRepository(
|
||
last: WorkoutHistorySetPerformance(
|
||
workoutHistoryId: 'history-1',
|
||
startedAt: DateTime.utc(2026, 7, 12),
|
||
setIndex: 0,
|
||
exerciseNameSnapshot: 'Squat',
|
||
scoreInputModeSnapshot: ScoreInputMode.stopwatch,
|
||
actualTimeMs: 72000,
|
||
actualScoreTimeMs: 45100,
|
||
),
|
||
record: WorkoutHistoryMetricPerformance(
|
||
workoutHistoryId: 'history-2',
|
||
startedAt: DateTime.utc(2026, 7, 15),
|
||
setIndex: 0,
|
||
exerciseNameSnapshot: 'Squat',
|
||
metric: PerformanceMetric.score,
|
||
scoreInputModeSnapshot: ScoreInputMode.stopwatch,
|
||
actualScoreTimeMs: 42800,
|
||
),
|
||
);
|
||
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(
|
||
sourceExerciseId: 'exercise-source-1',
|
||
scoreInputMode: ScoreInputMode.stopwatch,
|
||
targetTimeSeconds: 300,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
performanceReferenceUseCase: ExercisePerformanceReferenceUseCase(
|
||
repository: performanceRepository,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
await tester.pump();
|
||
|
||
expect(find.text('Durée de série : 05:00'), findsOneWidget);
|
||
expect(find.text('1:12 · 00:45.1'), findsOneWidget);
|
||
expect(find.text('00:42.8'), findsOneWidget);
|
||
expect(find.textContaining('72000'), findsNothing);
|
||
expect(find.textContaining('42800'), findsNothing);
|
||
});
|
||
|
||
testWidgets('les mesures cumulées respectent la hiérarchie visuelle', (
|
||
tester,
|
||
) async {
|
||
final scoreController = TextEditingController();
|
||
final scoreFocusNode = FocusNode();
|
||
addTearDown(scoreController.dispose);
|
||
addTearDown(scoreFocusNode.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: SetMeasureInput(
|
||
exercise: const ExecutionExercise(
|
||
id: 'exercise-1',
|
||
name: 'Squat',
|
||
setsCount: 3,
|
||
timeEnabled: true,
|
||
repsEnabled: true,
|
||
scoreEnabled: true,
|
||
restSeconds: 0,
|
||
scoreUnit: 'kg',
|
||
),
|
||
reps: 0,
|
||
scoreController: scoreController,
|
||
scoreFocusNode: scoreFocusNode,
|
||
onScoreSubmitted: (_) {},
|
||
onRepsChanged: (_) {},
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final timeY = tester.getTopLeft(find.text('Durée')).dy;
|
||
final repsY = tester.getTopLeft(find.text('Répétitions')).dy;
|
||
final scoreY = tester.getTopLeft(find.text('Score (kg)')).dy;
|
||
|
||
expect(timeY, lessThan(repsY));
|
||
expect(repsY, lessThan(scoreY));
|
||
});
|
||
|
||
testWidgets('aucun repos ne s’affiche après la toute dernière série', (
|
||
tester,
|
||
) async {
|
||
final plan = WorkoutExecutionPlan.fromSession(
|
||
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: 1,
|
||
restSeconds: 60,
|
||
),
|
||
),
|
||
);
|
||
const position = ExecutionPosition(
|
||
programIndex: 0,
|
||
exerciseIndex: 0,
|
||
setIndex: 0,
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
Directionality(
|
||
textDirection: TextDirection.ltr,
|
||
child: Text(
|
||
plan.shouldShowRestAfter(position) ? 'Repos' : 'Pas de repos',
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Pas de repos'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('le bouton médias est absent sans média', (tester) async {
|
||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||
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(),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(
|
||
_FakeActiveSessionRepository(),
|
||
clock,
|
||
),
|
||
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Voir médias'), findsNothing);
|
||
expect(find.byTooltip('Voir les médias de l’exercice'), findsNothing);
|
||
});
|
||
|
||
testWidgets('le bouton médias ouvre la sheet si médias présents', (
|
||
tester,
|
||
) async {
|
||
final imageFile = (await tester.runAsync(_createTestImageFile))!;
|
||
addTearDown(() async {
|
||
await tester.runAsync(() async {
|
||
await imageFile.parent.delete(recursive: true);
|
||
});
|
||
});
|
||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||
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(
|
||
imageMediaIds: const ['image-1'],
|
||
videoMediaId: 'video-1',
|
||
),
|
||
);
|
||
|
||
await tester.runAsync(() async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(
|
||
_FakeActiveSessionRepository(),
|
||
clock,
|
||
),
|
||
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
mediaAssetLoader: (id) async {
|
||
if (id == 'image-1') {
|
||
return MediaAsset(
|
||
metadata: _metadata(id),
|
||
kind: MediaKind.image,
|
||
localUri: imageFile.uri.toString(),
|
||
);
|
||
}
|
||
if (id == 'video-1') {
|
||
return MediaAsset(
|
||
metadata: _metadata(id),
|
||
kind: MediaKind.video,
|
||
localUri: imageFile.uri.toString(),
|
||
);
|
||
}
|
||
return null;
|
||
},
|
||
videoMediaBuilder: (context, asset) => SizedBox(
|
||
height: 260,
|
||
child: Center(child: Text('Lecteur vidéo ${asset.metadata.id}')),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
});
|
||
|
||
expect(find.byTooltip('Voir les médias de l’exercice'), findsOneWidget);
|
||
|
||
await tester.tap(find.byTooltip('Voir les médias de l’exercice'));
|
||
await tester.runAsync(() async {
|
||
await tester.pumpAndSettle();
|
||
});
|
||
|
||
expect(find.text('Médias de l’exercice'), findsOneWidget);
|
||
expect(find.text('Images'), findsOneWidget);
|
||
expect(find.text('Vidéo'), findsOneWidget);
|
||
expect(find.text('1/1'), findsOneWidget);
|
||
expect(
|
||
find.byKey(const ValueKey('exercise-image-image-1')),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.text('Image 1'), findsNothing);
|
||
|
||
await tester.tap(find.text('Vidéo'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Lecteur vidéo video-1'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('la galerie médias navigue entre les images', (tester) async {
|
||
final imageFile = (await tester.runAsync(_createTestImageFile))!;
|
||
addTearDown(() async {
|
||
await tester.runAsync(() async {
|
||
await imageFile.parent.delete(recursive: true);
|
||
});
|
||
});
|
||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||
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(
|
||
imageMediaIds: const ['image-1', 'image-2', 'image-3'],
|
||
),
|
||
);
|
||
|
||
await tester.runAsync(() async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(
|
||
_FakeActiveSessionRepository(),
|
||
clock,
|
||
),
|
||
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
mediaAssetLoader: (id) async => MediaAsset(
|
||
metadata: _metadata(id),
|
||
kind: MediaKind.image,
|
||
localUri: imageFile.uri.toString(),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
});
|
||
|
||
await tester.tap(find.byTooltip('Voir les médias de l’exercice'));
|
||
await tester.runAsync(() async {
|
||
await tester.pumpAndSettle();
|
||
});
|
||
|
||
expect(find.text('1/3'), findsOneWidget);
|
||
|
||
await tester.drag(find.byType(PageView), const Offset(-400, 0));
|
||
await tester.runAsync(() async {
|
||
await tester.pumpAndSettle();
|
||
});
|
||
|
||
expect(find.text('2/3'), findsOneWidget);
|
||
expect(
|
||
find.byKey(const ValueKey('exercise-image-image-2')),
|
||
findsOneWidget,
|
||
);
|
||
});
|
||
|
||
testWidgets('le chrono score démarre et s’arrête avec la bonne durée', (
|
||
tester,
|
||
) async {
|
||
await tester.binding.setSurfaceSize(const Size(400, 1400));
|
||
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(
|
||
timeEnabled: false,
|
||
scoreInputMode: ScoreInputMode.stopwatch,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Chrono score'), findsOneWidget);
|
||
expect(find.text('00:00.0'), findsOneWidget);
|
||
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Lancer'));
|
||
await tester.pump();
|
||
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 0, 100);
|
||
await tester.pump(const Duration(milliseconds: 100));
|
||
|
||
expect(find.text('00:00.1'), findsOneWidget);
|
||
|
||
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 0, 900);
|
||
await tester.pump(const Duration(milliseconds: 100));
|
||
|
||
expect(find.text('00:00.9'), findsOneWidget);
|
||
|
||
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1, 200);
|
||
await tester.pump(const Duration(seconds: 1));
|
||
|
||
expect(find.text('00:01.2'), findsOneWidget);
|
||
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Arrêter'));
|
||
await tester.pump();
|
||
|
||
expect(find.text('00:01.2'), findsOneWidget);
|
||
expect(
|
||
repository.scoreStopwatchStates.single.status,
|
||
ActiveScoreStopwatchStatus.stopped,
|
||
);
|
||
});
|
||
|
||
testWidgets('terminer sans chrono démarré affiche une confirmation', (
|
||
tester,
|
||
) async {
|
||
await tester.binding.setSurfaceSize(const Size(400, 1400));
|
||
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(
|
||
timeEnabled: false,
|
||
scoreInputMode: ScoreInputMode.stopwatch,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.text('Terminer la série'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Aucun temps chronométré'), findsOneWidget);
|
||
expect(find.text('Tu n’as pas démarré le chrono score.'), findsOneWidget);
|
||
expect(
|
||
find.widgetWithText(TextButton, 'Démarrer le chrono'),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.text('Terminer sans chrono'), findsOneWidget);
|
||
expect(repository.results, isEmpty);
|
||
});
|
||
|
||
testWidgets('terminer avec chrono en cours l’arrête et enregistre', (
|
||
tester,
|
||
) async {
|
||
await tester.binding.setSurfaceSize(const Size(400, 1400));
|
||
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,
|
||
timeEnabled: false,
|
||
scoreInputMode: ScoreInputMode.stopwatch,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Démarrer').first);
|
||
await tester.pump();
|
||
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 2, 500);
|
||
await tester.pump(const Duration(seconds: 1));
|
||
await tester.tap(find.text('Terminer la série'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(repository.results.single.actualScoreTimeMs, 2500);
|
||
expect(
|
||
repository.scoreStopwatchStates.single.status,
|
||
ActiveScoreStopwatchStatus.stopped,
|
||
);
|
||
expect(repository.session?.currentSetIndex, 1);
|
||
});
|
||
|
||
testWidgets('un repos actif est repris avec le temps restant recalculé', (
|
||
tester,
|
||
) async {
|
||
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: 60,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
repository.restStates.add(
|
||
ActiveRestState(
|
||
metadata: _metadata('rest-1'),
|
||
activeWorkoutSessionId: 'session-1',
|
||
afterProgramIndex: 0,
|
||
afterExerciseIndex: 0,
|
||
afterSetIndex: 0,
|
||
plannedRestSeconds: 60,
|
||
adjustedRestSeconds: 60,
|
||
startedAt: DateTime.now().toUtc().subtract(const Duration(seconds: 15)),
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
expect(find.text('Repos avant la prochaine série'), findsOneWidget);
|
||
expect(find.text('00:45'), findsOneWidget);
|
||
expect(find.text('Série 2 / 2'), findsOneWidget);
|
||
|
||
await tester.tap(find.text('+15 s'));
|
||
await tester.pump();
|
||
|
||
expect(find.text('01:00'), findsOneWidget);
|
||
expect(repository.restStates.single.adjustedRestSeconds, 75);
|
||
|
||
await tester.tap(find.text('Ignorer le repos'));
|
||
await tester.pump();
|
||
|
||
expect(repository.restStates.single.skippedAt, isNotNull);
|
||
});
|
||
|
||
testWidgets('les mutations externes de repos resynchronisent l’écran', (
|
||
tester,
|
||
) async {
|
||
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: 60,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
expect(find.text('Repos avant la prochaine série'), findsNothing);
|
||
repository.restStates.add(
|
||
ActiveRestState(
|
||
metadata: _metadata('rest-1'),
|
||
activeWorkoutSessionId: 'session-1',
|
||
afterProgramIndex: 0,
|
||
afterExerciseIndex: 0,
|
||
afterSetIndex: 0,
|
||
plannedRestSeconds: 60,
|
||
adjustedRestSeconds: 60,
|
||
startedAt: DateTime.now().toUtc(),
|
||
),
|
||
);
|
||
|
||
await tester.pump(const Duration(seconds: 1));
|
||
await tester.pump();
|
||
|
||
expect(find.text('Repos avant la prochaine série'), findsOneWidget);
|
||
|
||
repository.restStates.clear();
|
||
repository.session = ActiveWorkoutSession(
|
||
metadata: session.metadata,
|
||
sourceWorkoutTemplateId: session.sourceWorkoutTemplateId,
|
||
status: ActiveWorkoutStatus.running,
|
||
startedAt: session.startedAt,
|
||
lastPersistedAt: session.lastPersistedAt,
|
||
elapsedActiveMs: session.elapsedActiveMs,
|
||
currentProgramIndex: 0,
|
||
currentExerciseIndex: 0,
|
||
currentSetIndex: 1,
|
||
resolvedTemplateSnapshotJson: session.resolvedTemplateSnapshotJson,
|
||
);
|
||
|
||
await tester.pump(const Duration(seconds: 1));
|
||
await tester.pump();
|
||
|
||
expect(find.text('Repos avant la prochaine série'), findsNothing);
|
||
expect(find.text('Terminer la série'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('un repos en pause est repris sans compter la durée de pause', (
|
||
tester,
|
||
) async {
|
||
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: 60,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
final now = DateTime.now().toUtc();
|
||
repository.restStates.add(
|
||
ActiveRestState(
|
||
metadata: _metadata('rest-1'),
|
||
activeWorkoutSessionId: 'session-1',
|
||
afterProgramIndex: 0,
|
||
afterExerciseIndex: 0,
|
||
afterSetIndex: 0,
|
||
plannedRestSeconds: 60,
|
||
adjustedRestSeconds: 60,
|
||
startedAt: now.subtract(const Duration(minutes: 10, seconds: 15)),
|
||
pausedAt: now.subtract(const Duration(minutes: 10)),
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
expect(find.text('Repos avant la prochaine série'), findsOneWidget);
|
||
expect(find.text('00:45'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('la mesure temps enregistre une durée réelle de série', (
|
||
tester,
|
||
) async {
|
||
await tester.binding.setSurfaceSize(const Size(400, 1400));
|
||
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,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Démarrer').first);
|
||
await tester.pump();
|
||
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 2);
|
||
await tester.pump(const Duration(seconds: 2));
|
||
await tester.tap(find.text('Terminer la série'));
|
||
await tester.pump();
|
||
|
||
expect(repository.results.single.actualTimeMs, 2000);
|
||
});
|
||
|
||
testWidgets(
|
||
'durée et score chrono utilisent un seul chrono visible en exécution',
|
||
(tester) async {
|
||
await tester.binding.setSurfaceSize(const Size(400, 1400));
|
||
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,
|
||
timeEnabled: true,
|
||
scoreInputMode: ScoreInputMode.stopwatch,
|
||
targetTimeSeconds: 30,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Chrono score'), findsNothing);
|
||
expect(find.text('Durée de série : 00:30'), findsOneWidget);
|
||
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Démarrer').first);
|
||
await tester.pump();
|
||
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 2);
|
||
await tester.pump(const Duration(seconds: 2));
|
||
|
||
expect(find.text('Chrono score'), findsNothing);
|
||
expect(find.text('Durée de série : 00:02'), findsOneWidget);
|
||
|
||
await tester.tap(find.text('Terminer la série'));
|
||
await tester.pump();
|
||
|
||
final result = repository.results.single;
|
||
expect(result.actualTimeMs, 2000);
|
||
expect(result.actualScoreTimeMs, 2000);
|
||
},
|
||
);
|
||
|
||
testWidgets(
|
||
'terminer la série 2 avec temps répétitions et score manuel avance',
|
||
(tester) async {
|
||
await tester.binding.setSurfaceSize(const Size(400, 1400));
|
||
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: 1,
|
||
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||
setsCount: 3,
|
||
restSeconds: 0,
|
||
targetTimeSeconds: 30,
|
||
targetReps: 5,
|
||
targetScore: 10,
|
||
scoreUnit: 'points',
|
||
),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.text('Programme jambes'), findsOneWidget);
|
||
expect(find.text('2 / 3'), findsOneWidget);
|
||
expect(find.text('Squat'), findsOneWidget);
|
||
expect(find.text('Durée de série : 00:30'), findsOneWidget);
|
||
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Démarrer').first);
|
||
await tester.pump();
|
||
|
||
for (var i = 0; i < 2; i++) {
|
||
await tester.tap(find.byIcon(Icons.remove));
|
||
await tester.pump();
|
||
}
|
||
await tester.enterText(
|
||
find.widgetWithText(TextField, 'Score (points)'),
|
||
'5',
|
||
);
|
||
await tester.ensureVisible(find.text('Terminer la série'));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.text('Terminer la série'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(repository.results.single.setIndex, 1);
|
||
expect(repository.results.single.actualReps, 3);
|
||
expect(repository.results.single.actualScore, 5);
|
||
expect(repository.session?.currentSetIndex, 2);
|
||
expect(find.text('Programme jambes'), findsOneWidget);
|
||
expect(find.text('3 / 3'), findsOneWidget);
|
||
expect(find.text('5'), findsOneWidget);
|
||
},
|
||
);
|
||
|
||
testWidgets(
|
||
'la saisie du score téléphone alimente le score vivant de la montre',
|
||
(tester) async {
|
||
await tester.binding.setSurfaceSize(const Size(400, 1400));
|
||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||
|
||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||
final repository = _FakeActiveSessionRepository();
|
||
final activeUseCases = _activeUseCases(repository, clock);
|
||
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(
|
||
restSeconds: 0,
|
||
scoreUnit: 'points',
|
||
),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: activeUseCases,
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.enterText(
|
||
find.widgetWithText(TextField, 'Score (points)'),
|
||
'7.5',
|
||
);
|
||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||
await tester.pump();
|
||
|
||
expect(repository.manualScoreStates.single.value, 7.5);
|
||
|
||
final incremented = await activeUseCases.incrementManualScore(
|
||
sessionId: 'session-1',
|
||
programIndex: 0,
|
||
exerciseIndex: 0,
|
||
setIndex: 0,
|
||
);
|
||
expect(incremented.state.value, 8.5);
|
||
},
|
||
);
|
||
|
||
testWidgets('la flèche de retour met la séance active en pause', (
|
||
tester,
|
||
) async {
|
||
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,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byTooltip('Mettre en pause'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Reprendre'), findsOneWidget);
|
||
expect(repository.session?.status, ActiveWorkoutStatus.paused);
|
||
});
|
||
|
||
testWidgets('le plan de séance affiche les états des séries', (tester) async {
|
||
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: 2,
|
||
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||
setsCount: 4,
|
||
restSeconds: 0,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
repository.results.add(
|
||
ActiveSetResult(
|
||
metadata: _metadata('result-1'),
|
||
activeWorkoutSessionId: 'session-1',
|
||
programSnapshotId: 'template-program-1',
|
||
exerciseSnapshotId: 'exercise-1',
|
||
programIndex: 0,
|
||
exerciseIndex: 0,
|
||
setIndex: 0,
|
||
actualReps: 10,
|
||
actualScore: 7,
|
||
scoreUnitSnapshot: 'paniers',
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byTooltip('Voir le plan'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Plan de séance'), findsOneWidget);
|
||
expect(find.text('Terminée'), findsOneWidget);
|
||
expect(find.text('10 reps · 7 paniers'), findsOneWidget);
|
||
expect(find.text('Passée'), findsOneWidget);
|
||
expect(find.text('Aucun résultat'), findsOneWidget);
|
||
expect(find.text('En cours'), findsOneWidget);
|
||
expect(find.text('À faire'), findsOneWidget);
|
||
|
||
await tester.tap(find.text('En cours'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Plan de séance'), findsNothing);
|
||
});
|
||
|
||
testWidgets('le plan affiche le score chrono formaté avec objectif', (
|
||
tester,
|
||
) async {
|
||
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: 1,
|
||
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||
setsCount: 2,
|
||
restSeconds: 0,
|
||
scoreInputMode: ScoreInputMode.stopwatch,
|
||
targetScoreTimeMs: 45000,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
repository.results.add(
|
||
ActiveSetResult(
|
||
metadata: _metadata('result-1'),
|
||
activeWorkoutSessionId: 'session-1',
|
||
programSnapshotId: 'template-program-1',
|
||
exerciseSnapshotId: 'exercise-1',
|
||
programIndex: 0,
|
||
exerciseIndex: 0,
|
||
setIndex: 0,
|
||
actualScoreTimeMs: 38700,
|
||
scoreInputModeSnapshot: ScoreInputMode.stopwatch,
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byTooltip('Voir le plan'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Score chrono 00:38.7 / objectif 00:45'), findsOneWidget);
|
||
expect(find.textContaining('Temps réalisé 00:38.7'), findsNothing);
|
||
});
|
||
|
||
testWidgets('le plan affiche le résumé de séquence d’une série', (
|
||
tester,
|
||
) async {
|
||
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,
|
||
targetReps: 2,
|
||
exerciseSteps: [
|
||
_step('step-1', 0, 'Gauche', ExerciseStepType.reps, 4),
|
||
_step('step-2', 1, 'Droite', ExerciseStepType.reps, 4),
|
||
],
|
||
),
|
||
);
|
||
repository.session = session;
|
||
repository.stepResults.add(
|
||
ActiveExerciseStepResult(
|
||
metadata: _metadata('step-result-1'),
|
||
activeWorkoutSessionId: 'session-1',
|
||
programSnapshotId: 'template-program-1',
|
||
exerciseSnapshotId: 'exercise-1',
|
||
programIndex: 0,
|
||
exerciseIndex: 0,
|
||
setIndex: 0,
|
||
passageIndex: 0,
|
||
stepIndex: 0,
|
||
stepSnapshotId: 'step-1',
|
||
stepNameSnapshot: 'Gauche',
|
||
stepTypeSnapshot: ExerciseStepType.reps,
|
||
targetValueSnapshot: 4,
|
||
hasScoreSnapshot: false,
|
||
status: SetResultStatus.completed,
|
||
actualReps: 4,
|
||
),
|
||
);
|
||
|
||
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.tap(find.byTooltip('Voir le plan'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Séquence : 1/2 étapes · Passage 1/2'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets(
|
||
'modifier une série passée la termine sans changer la position courante',
|
||
(tester) async {
|
||
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: 2,
|
||
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||
setsCount: 4,
|
||
restSeconds: 0,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byTooltip('Voir le plan'));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.widgetWithText(ListTile, 'Série 2'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Modifier la série'), findsOneWidget);
|
||
|
||
await tester.enterText(_textFieldWithLabel('Répétitions'), '12');
|
||
await tester.ensureVisible(find.text('Enregistrer le résultat'));
|
||
await tester.tap(find.text('Enregistrer le résultat'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(repository.session?.currentSetIndex, 2);
|
||
expect(repository.results.single.status, SetResultStatus.completed);
|
||
expect(repository.results.single.actualReps, 12);
|
||
expect(find.text('Série mise à jour'), findsWidgets);
|
||
expect(find.text('12 reps'), findsOneWidget);
|
||
|
||
await tester.tap(find.byTooltip('Fermer'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Programme jambes'), findsOneWidget);
|
||
expect(find.text('3 / 4'), findsOneWidget);
|
||
},
|
||
);
|
||
|
||
testWidgets(
|
||
'toucher la série courante dans le plan revient à l’écran principal',
|
||
(tester) async {
|
||
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: 1,
|
||
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||
setsCount: 3,
|
||
restSeconds: 0,
|
||
),
|
||
);
|
||
repository.session = session;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: WorkoutExecutionScreen(
|
||
initialSession: session,
|
||
activeUseCases: _activeUseCases(repository, clock),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byTooltip('Voir le plan'));
|
||
await tester.pumpAndSettle();
|
||
await tester.tap(find.text('En cours'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Plan de séance'), findsNothing);
|
||
expect(find.text('Modifier la série'), findsNothing);
|
||
expect(find.text('Programme jambes'), 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.byType(ListView), findsNothing);
|
||
expect(find.text('SÉQUENCE'), findsOneWidget);
|
||
expect(find.text('Passage 1 / 10'), findsOneWidget);
|
||
expect(find.text('ÉTAPE 1 / 2'), findsNothing);
|
||
expect(find.text('Dribble main droite'), findsOneWidget);
|
||
expect(find.text('00:10'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('un exercice dense avec étapes et scores reste scrollable', (
|
||
tester,
|
||
) async {
|
||
await tester.binding.setSurfaceSize(const Size(400, 700));
|
||
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: 3,
|
||
targetReps: 20,
|
||
scoreEnabled: true,
|
||
scoreUnit: 'paniers',
|
||
exerciseSteps: [
|
||
_step(
|
||
'step-1',
|
||
0,
|
||
'Tirs après dribble',
|
||
ExerciseStepType.time,
|
||
10,
|
||
hasScore: true,
|
||
scoreLabel: 'Paniers',
|
||
scoreUnit: 'paniers',
|
||
defaultTargetScore: 8,
|
||
),
|
||
_step('step-2', 1, 'Replacement défensif', ExerciseStepType.reps, 6),
|
||
],
|
||
),
|
||
);
|
||
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(tester.takeException(), isNull);
|
||
expect(find.text('ÉTAPE 1 / 2'), findsNothing);
|
||
expect(find.text('Tirs après dribble'), findsOneWidget);
|
||
expect(find.text('00:10'), findsOneWidget);
|
||
expect(find.text('Passer l’étape'), findsOneWidget);
|
||
expect(find.byType(SingleChildScrollView), findsWidgets);
|
||
});
|
||
|
||
testWidgets('répétitions et chrono score d’étape tiennent sans overflow', (
|
||
tester,
|
||
) async {
|
||
await tester.binding.setSurfaceSize(const Size(400, 700));
|
||
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: 12,
|
||
exerciseSteps: [
|
||
_step(
|
||
'step-1',
|
||
0,
|
||
'Pompes tempo',
|
||
ExerciseStepType.reps,
|
||
12,
|
||
hasScore: true,
|
||
scoreInputMode: ScoreInputMode.stopwatch,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
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(tester.takeException(), isNull);
|
||
expect(find.text('RÉPÉTITIONS'), findsOneWidget);
|
||
expect(find.text('CHRONO SCORE'), findsOneWidget);
|
||
expect(find.byTooltip('Lancer'), findsOneWidget);
|
||
expect(find.byTooltip('Réinitialiser'), findsOneWidget);
|
||
expect(
|
||
find.widgetWithText(OutlinedButton, 'Passer l’étape'),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.widgetWithText(FilledButton, 'Étape suivante'), findsOneWidget);
|
||
expect(find.widgetWithText(TextButton, 'Réinitialiser'), findsNothing);
|
||
});
|
||
|
||
testWidgets('chrono décompte et score libre tiennent avec stepper compact', (
|
||
tester,
|
||
) async {
|
||
await tester.binding.setSurfaceSize(const Size(400, 700));
|
||
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(
|
||
timeEnabled: false,
|
||
repsEnabled: false,
|
||
scoreEnabled: false,
|
||
exerciseSteps: [
|
||
_step(
|
||
'step-1',
|
||
0,
|
||
'Maintien défensif',
|
||
ExerciseStepType.time,
|
||
10,
|
||
hasScore: true,
|
||
scoreInputMode: ScoreInputMode.manual,
|
||
scoreLabel: 'Paniers',
|
||
scoreUnit: 'points',
|
||
defaultTargetScore: 5,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
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(tester.takeException(), isNull);
|
||
expect(find.text('CHRONO'), findsOneWidget);
|
||
expect(find.text('SCORE'), findsOneWidget);
|
||
expect(find.text('00:10'), findsOneWidget);
|
||
expect(find.text('5'), findsOneWidget);
|
||
expect(find.byTooltip('Diminuer'), findsOneWidget);
|
||
expect(find.byTooltip('Augmenter'), findsOneWidget);
|
||
expect(find.widgetWithText(TextField, 'Score de l’étape'), findsNothing);
|
||
expect(
|
||
find.widgetWithText(OutlinedButton, 'Passer l’étape'),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.widgetWithText(FilledButton, 'Étape suivante'), findsNothing);
|
||
});
|
||
|
||
testWidgets('l’écran actif avec séquence longue reste sans scroll', (
|
||
tester,
|
||
) async {
|
||
await tester.binding.setSurfaceSize(const Size(400, 900));
|
||
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: 4,
|
||
workoutName: 'Séance tirs du soir très complète',
|
||
programName:
|
||
'Programme finition longue avec nom volontairement interminable',
|
||
exerciseName: 'Dribble combo finition contact côté faible très long',
|
||
targetReps: 10,
|
||
scoreUnit: 'paniers',
|
||
exerciseSteps: [
|
||
_step(
|
||
'step-1',
|
||
0,
|
||
'Dribble main droite enchaîné avec finition longue',
|
||
ExerciseStepType.time,
|
||
90,
|
||
),
|
||
_step('step-2', 1, 'Appuis explosifs', ExerciseStepType.reps, 8),
|
||
_step('step-3', 2, 'Tir après contact', ExerciseStepType.reps, 6),
|
||
],
|
||
),
|
||
);
|
||
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(tester.takeException(), isNull);
|
||
expect(find.byType(ListView), findsNothing);
|
||
expect(find.text('SÉRIE'), findsOneWidget);
|
||
expect(find.text('Passage 1 / 10'), findsOneWidget);
|
||
expect(find.text('Objectif : 1:30'), findsOneWidget);
|
||
expect(find.widgetWithText(FilledButton, 'Démarrer'), findsOneWidget);
|
||
expect(find.widgetWithText(FilledButton, 'Lancer'), findsOneWidget);
|
||
expect(find.textContaining('Passages réalisés'), findsNothing);
|
||
});
|
||
|
||
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);
|
||
expect(find.byType(SingleChildScrollView), findsNothing);
|
||
expect(
|
||
find.widgetWithText(OutlinedButton, 'Passer l’étape'),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.widgetWithText(FilledButton, 'Étape suivante'), findsOneWidget);
|
||
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.widgetWithText(FilledButton, 'Lancer'));
|
||
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);
|
||
|
||
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 3);
|
||
await tester.pump(const Duration(milliseconds: 100));
|
||
|
||
expect(find.text('00:02'), findsOneWidget);
|
||
expect(audio.shortBeeps, 2);
|
||
|
||
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 4);
|
||
await tester.pump(const Duration(milliseconds: 100));
|
||
|
||
expect(find.text('00:01'), findsOneWidget);
|
||
expect(audio.shortBeeps, 3);
|
||
|
||
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 5, 200);
|
||
await tester.pump(const Duration(milliseconds: 200));
|
||
await tester.pump();
|
||
|
||
expect(audio.longBeeps, 1);
|
||
});
|
||
|
||
testWidgets(
|
||
'terminer sans démarrer la première étape temps affiche une confirmation',
|
||
(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(
|
||
timeEnabled: false,
|
||
scoreEnabled: false,
|
||
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();
|
||
|
||
await tester.tap(find.text('Terminer la série'));
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(find.text('Chrono non lancé'), findsOneWidget);
|
||
expect(
|
||
find.text('Tu n’as pas démarré le chrono de cette série.'),
|
||
findsOneWidget,
|
||
);
|
||
expect(find.widgetWithText(TextButton, 'Démarrer'), findsOneWidget);
|
||
expect(find.text('Terminer sans chrono'), findsOneWidget);
|
||
expect(repository.results, isEmpty);
|
||
},
|
||
);
|
||
|
||
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.widgetWithText(FilledButton, 'Lancer'));
|
||
await tester.pump();
|
||
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1, 200);
|
||
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'), findsNothing);
|
||
expect(find.text('Droite'), findsOneWidget);
|
||
expect(find.text('00:02'), findsOneWidget);
|
||
expect(
|
||
repository.stepProgressStates.single.status,
|
||
ActiveExerciseStepProgressStatus.runningTimer,
|
||
);
|
||
expect(audio.longBeeps, 1);
|
||
});
|
||
|
||
testWidgets(
|
||
'chrono suivant prêt attend un démarrage manuel quand l’enchaînement est désactivé',
|
||
(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,
|
||
autoStartNextTimedStep: false,
|
||
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: _FakeStepAudioCuePlayer(),
|
||
closeUseCase: _closeUseCase(repository, clock),
|
||
historyUseCases: _historyUseCases(clock),
|
||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||
),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Lancer'));
|
||
await tester.pump();
|
||
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1, 200);
|
||
await tester.pump(const Duration(milliseconds: 200));
|
||
await tester.pump();
|
||
|
||
expect(repository.stepResults.single.stepIndex, 0);
|
||
expect(find.text('ÉTAPE 2 / 2'), findsNothing);
|
||
expect(find.text('Droite'), findsOneWidget);
|
||
expect(find.text('00:02'), findsOneWidget);
|
||
expect(find.text('Chrono prêt'), findsOneWidget);
|
||
expect(find.widgetWithText(FilledButton, 'Lancer'), findsOneWidget);
|
||
expect(
|
||
repository.stepProgressStates.single.status,
|
||
ActiveExerciseStepProgressStatus.stoppedTimer,
|
||
);
|
||
expect(repository.stepProgressStates.single.startedAt, isNull);
|
||
|
||
await tester.tap(find.widgetWithText(FilledButton, 'Lancer'));
|
||
await tester.pump();
|
||
|
||
expect(
|
||
repository.stepProgressStates.single.status,
|
||
ActiveExerciseStepProgressStatus.runningTimer,
|
||
);
|
||
expect(repository.stepProgressStates.single.startedAt, clock.value);
|
||
},
|
||
);
|
||
|
||
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.textContaining('Passages réalisés'), findsNothing);
|
||
});
|
||
|
||
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.byTooltip('Plus d’actions'));
|
||
await tester.pumpAndSettle();
|
||
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.byTooltip('Plus d’actions'));
|
||
await tester.pumpAndSettle();
|
||
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) {
|
||
return find.byWidgetPredicate(
|
||
(widget) => widget is TextField && widget.decoration?.labelText == label,
|
||
description: 'TextField with label "$label"',
|
||
);
|
||
}
|
||
|
||
Future<File> _createTestImageFile() async {
|
||
final directory = await Directory.systemTemp.createTemp('gametime-media-');
|
||
final file = File('${directory.path}/image.png');
|
||
const transparentPng =
|
||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ'
|
||
'AAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
||
await file.writeAsBytes(base64Decode(transparentPng));
|
||
return file;
|
||
}
|
||
|
||
ActiveWorkoutSessionUseCases _activeUseCases(
|
||
_FakeActiveSessionRepository repository,
|
||
_FakeClock clock,
|
||
) {
|
||
return ActiveWorkoutSessionUseCases(
|
||
sessionRepository: repository,
|
||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||
clock: clock,
|
||
ids: _FakeIds(),
|
||
originDeviceId: 'device-1',
|
||
);
|
||
}
|
||
|
||
CloseWorkoutSessionUseCase _closeUseCase(
|
||
_FakeActiveSessionRepository repository,
|
||
_FakeClock clock,
|
||
) {
|
||
return CloseWorkoutSessionUseCase(
|
||
sessionRepository: repository,
|
||
historyRepository: _FakeWorkoutHistoryRepository(),
|
||
clock: clock,
|
||
ids: _FakeIds(),
|
||
originDeviceId: 'device-1',
|
||
);
|
||
}
|
||
|
||
ActiveExerciseStepUseCases _stepUseCases(
|
||
_FakeActiveSessionRepository repository,
|
||
_FakeClock clock,
|
||
) {
|
||
return ActiveExerciseStepUseCases(
|
||
sessionRepository: repository,
|
||
clock: clock,
|
||
ids: _FakeIds(),
|
||
originDeviceId: 'device-1',
|
||
);
|
||
}
|
||
|
||
WorkoutHistoryUseCases _historyUseCases(_FakeClock clock) {
|
||
return WorkoutHistoryUseCases(
|
||
repository: _FakeWorkoutHistoryRepository(),
|
||
clock: clock,
|
||
);
|
||
}
|
||
|
||
WorkoutTemplateUseCases _workoutTemplateUseCases() {
|
||
return WorkoutTemplateUseCases(
|
||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||
programRepository: _FakeProgramRepository(),
|
||
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
|
||
ids: _FakeIds(),
|
||
originDeviceId: 'device-1',
|
||
);
|
||
}
|
||
|
||
String _sessionSnapshot({
|
||
int setsCount = 3,
|
||
int restSeconds = 0,
|
||
bool timeEnabled = true,
|
||
bool repsEnabled = true,
|
||
bool scoreEnabled = true,
|
||
int targetTimeSeconds = 45,
|
||
int targetReps = 10,
|
||
double targetScore = 80,
|
||
int? targetScoreTimeMs,
|
||
String scoreUnit = 'kg',
|
||
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
||
List<String> imageMediaIds = const [],
|
||
List<Map<String, Object?>> exerciseSteps = const [],
|
||
bool autoStartNextTimedStep = true,
|
||
String? videoMediaId,
|
||
String? sourceExerciseId,
|
||
String workoutName = 'Séance jambes',
|
||
String programName = 'Programme jambes',
|
||
String exerciseName = 'Squat',
|
||
}) {
|
||
return jsonEncode({
|
||
'name': workoutName,
|
||
'programs': [
|
||
{
|
||
'id': 'template-program-1',
|
||
'programNameSnapshot': programName,
|
||
'programSnapshotJson': jsonEncode({
|
||
'exercises': [
|
||
{
|
||
'id': 'exercise-1',
|
||
'sourceExerciseId': sourceExerciseId,
|
||
'exerciseNameSnapshot': exerciseName,
|
||
'setsCount': setsCount,
|
||
'timeEnabled': timeEnabled,
|
||
'repsEnabled': repsEnabled,
|
||
'scoreEnabled': scoreEnabled,
|
||
'targetTimeSeconds': targetTimeSeconds,
|
||
'targetReps': targetReps,
|
||
'targetScore': targetScore,
|
||
'targetScoreTimeMs': targetScoreTimeMs,
|
||
'scoreInputModeSnapshot': scoreInputMode.name,
|
||
'scoreLabelSnapshot': 'Score',
|
||
'scoreUnitSnapshot': scoreUnit,
|
||
'restSecondsOverride': restSeconds,
|
||
'exerciseImageMediaIdsSnapshot': imageMediaIds,
|
||
'imageMediaIdsSnapshot': imageMediaIds,
|
||
'exerciseVideoMediaIdSnapshot': videoMediaId,
|
||
'videoMediaIdSnapshot': videoMediaId,
|
||
'exerciseStepsSnapshot': exerciseSteps,
|
||
'autoStartNextTimedStepSnapshot': autoStartNextTimedStep,
|
||
},
|
||
],
|
||
}),
|
||
},
|
||
],
|
||
'overrides': const [],
|
||
});
|
||
}
|
||
|
||
String _emptySessionSnapshot() {
|
||
return jsonEncode({
|
||
'name': 'Séance vide',
|
||
'programs': [
|
||
{
|
||
'id': 'template-program-1',
|
||
'programNameSnapshot': 'Programme vide',
|
||
'programSnapshotJson': jsonEncode({'exercises': const []}),
|
||
},
|
||
],
|
||
'overrides': const [],
|
||
});
|
||
}
|
||
|
||
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) {
|
||
return EntityMetadata(
|
||
id: id,
|
||
createdAt: DateTime.utc(2026, 7, 17),
|
||
updatedAt: DateTime.utc(2026, 7, 17),
|
||
originDeviceId: 'device-1',
|
||
);
|
||
}
|
||
|
||
final class _FakeClock implements Clock {
|
||
_FakeClock(this.value);
|
||
|
||
DateTime value;
|
||
|
||
@override
|
||
DateTime now() => value;
|
||
}
|
||
|
||
final class _FakeIds implements IdGenerator {
|
||
var _next = 0;
|
||
|
||
@override
|
||
String newId() {
|
||
_next += 1;
|
||
return 'id-$_next';
|
||
}
|
||
}
|
||
|
||
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 _FakeExercisePerformanceReferenceRepository
|
||
implements ExercisePerformanceReferenceRepository {
|
||
const _FakeExercisePerformanceReferenceRepository({this.last, this.record});
|
||
|
||
final WorkoutHistorySetPerformance? last;
|
||
final WorkoutHistoryMetricPerformance? record;
|
||
|
||
@override
|
||
Future<bool> hasAnyCompletedHistoryForExercise(String exerciseId) async {
|
||
return true;
|
||
}
|
||
|
||
@override
|
||
Future<WorkoutHistorySetPerformance?> findLatestSetPerformance({
|
||
required String exerciseId,
|
||
required ActivePerformanceMeasures activeMeasures,
|
||
required int currentSetIndex,
|
||
}) async {
|
||
return last;
|
||
}
|
||
|
||
@override
|
||
Future<WorkoutHistoryMetricPerformance?> findBestMetricPerformance({
|
||
required String exerciseId,
|
||
required PerformanceMetric metric,
|
||
required ScoreInputMode scoreInputMode,
|
||
}) async {
|
||
return record;
|
||
}
|
||
}
|
||
|
||
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
||
ActiveWorkoutSession? session;
|
||
final results = <ActiveSetResult>[];
|
||
final restStates = <ActiveRestState>[];
|
||
final scoreStopwatchStates = <ActiveScoreStopwatchState>[];
|
||
final manualScoreStates = <ActiveManualScoreState>[];
|
||
final setTimerStates = <ActiveSetTimerState>[];
|
||
final stepProgressStates = <ActiveExerciseStepProgressState>[];
|
||
final stepResults = <ActiveExerciseStepResult>[];
|
||
|
||
@override
|
||
Future<ActiveWorkoutSession?> findById(String id) async {
|
||
return session?.metadata.id == id ? session : null;
|
||
}
|
||
|
||
@override
|
||
Future<ActiveWorkoutSession?> findOpen() async => session;
|
||
|
||
@override
|
||
Future<ActiveRestState?> findRestStateById(String id) async {
|
||
for (final restState in restStates) {
|
||
if (restState.metadata.id == id) {
|
||
return restState;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
@override
|
||
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
}) async {
|
||
for (final state in scoreStopwatchStates) {
|
||
if (state.activeWorkoutSessionId == sessionId &&
|
||
state.programIndex == programIndex &&
|
||
state.exerciseIndex == exerciseIndex &&
|
||
state.setIndex == setIndex &&
|
||
state.metadata.deletedAt == null) {
|
||
return state;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
@override
|
||
Future<ActiveManualScoreState?> findManualScoreState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
}) async {
|
||
for (final state in manualScoreStates) {
|
||
if (state.activeWorkoutSessionId == sessionId &&
|
||
state.programIndex == programIndex &&
|
||
state.exerciseIndex == exerciseIndex &&
|
||
state.setIndex == setIndex &&
|
||
state.metadata.deletedAt == null) {
|
||
return state;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
@override
|
||
Future<ActiveSetTimerState?> findSetTimerState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
}) async {
|
||
for (final state in setTimerStates) {
|
||
if (state.activeWorkoutSessionId == sessionId &&
|
||
state.programIndex == programIndex &&
|
||
state.exerciseIndex == exerciseIndex &&
|
||
state.setIndex == setIndex &&
|
||
state.metadata.deletedAt == null) {
|
||
return state;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
@override
|
||
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
}) async {
|
||
for (final state in stepProgressStates) {
|
||
if (state.activeWorkoutSessionId == sessionId &&
|
||
state.programIndex == programIndex &&
|
||
state.exerciseIndex == exerciseIndex &&
|
||
state.setIndex == setIndex &&
|
||
state.metadata.deletedAt == null) {
|
||
return state;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||
return restStates
|
||
.where((restState) => restState.activeWorkoutSessionId == sessionId)
|
||
.toList();
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||
String sessionId,
|
||
) async {
|
||
return scoreStopwatchStates
|
||
.where((state) => state.activeWorkoutSessionId == sessionId)
|
||
.toList();
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveManualScoreState>> listManualScoreStates(
|
||
String sessionId,
|
||
) async {
|
||
return manualScoreStates
|
||
.where((state) => state.activeWorkoutSessionId == sessionId)
|
||
.toList();
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||
return results;
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveSetTimerState>> listSetTimerStates(String sessionId) async {
|
||
return setTimerStates
|
||
.where((state) => state.activeWorkoutSessionId == sessionId)
|
||
.toList();
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
|
||
String sessionId,
|
||
) async {
|
||
return stepProgressStates
|
||
.where((state) => state.activeWorkoutSessionId == sessionId)
|
||
.toList();
|
||
}
|
||
|
||
@override
|
||
Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
|
||
String sessionId,
|
||
) async {
|
||
return stepResults
|
||
.where((result) => result.activeWorkoutSessionId == sessionId)
|
||
.toList();
|
||
}
|
||
|
||
@override
|
||
Future<void> save(ActiveWorkoutSession session) async {
|
||
this.session = session;
|
||
}
|
||
|
||
@override
|
||
Future<void> saveRestState(ActiveRestState restState) async {
|
||
final index = restStates.indexWhere(
|
||
(saved) => saved.metadata.id == restState.metadata.id,
|
||
);
|
||
if (index == -1) {
|
||
restStates.add(restState);
|
||
} else {
|
||
restStates[index] = restState;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {
|
||
final index = scoreStopwatchStates.indexWhere(
|
||
(saved) => saved.metadata.id == state.metadata.id,
|
||
);
|
||
if (index == -1) {
|
||
scoreStopwatchStates.add(state);
|
||
} else {
|
||
scoreStopwatchStates[index] = state;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<void> saveManualScoreState(ActiveManualScoreState state) async {
|
||
final index = manualScoreStates.indexWhere(
|
||
(saved) => saved.metadata.id == state.metadata.id,
|
||
);
|
||
if (index == -1) {
|
||
manualScoreStates.add(state);
|
||
} else {
|
||
manualScoreStates[index] = state;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<void> saveSetTimerState(ActiveSetTimerState state) async {
|
||
final index = setTimerStates.indexWhere(
|
||
(saved) =>
|
||
saved.activeWorkoutSessionId == state.activeWorkoutSessionId &&
|
||
saved.programIndex == state.programIndex &&
|
||
saved.exerciseIndex == state.exerciseIndex &&
|
||
saved.setIndex == state.setIndex,
|
||
);
|
||
if (index == -1) {
|
||
setTimerStates.add(state);
|
||
} else {
|
||
setTimerStates[index] = state;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<void> saveExerciseStepProgressState(
|
||
ActiveExerciseStepProgressState state,
|
||
) async {
|
||
final index = stepProgressStates.indexWhere(
|
||
(saved) =>
|
||
saved.activeWorkoutSessionId == state.activeWorkoutSessionId &&
|
||
saved.programIndex == state.programIndex &&
|
||
saved.exerciseIndex == state.exerciseIndex &&
|
||
saved.setIndex == state.setIndex,
|
||
);
|
||
if (index == -1) {
|
||
stepProgressStates.add(state);
|
||
} else {
|
||
stepProgressStates[index] = state;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {
|
||
final index = stepResults.indexWhere(
|
||
(saved) =>
|
||
saved.activeWorkoutSessionId == result.activeWorkoutSessionId &&
|
||
saved.programIndex == result.programIndex &&
|
||
saved.exerciseIndex == result.exerciseIndex &&
|
||
saved.setIndex == result.setIndex &&
|
||
saved.passageIndex == result.passageIndex &&
|
||
saved.stepIndex == result.stepIndex,
|
||
);
|
||
if (index == -1) {
|
||
stepResults.add(result);
|
||
} else {
|
||
stepResults[index] = result;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<void> deleteScoreStopwatchState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
required DateTime deletedAt,
|
||
}) async {
|
||
scoreStopwatchStates.removeWhere(
|
||
(state) =>
|
||
state.activeWorkoutSessionId == sessionId &&
|
||
state.programIndex == programIndex &&
|
||
state.exerciseIndex == exerciseIndex &&
|
||
state.setIndex == setIndex,
|
||
);
|
||
}
|
||
|
||
@override
|
||
Future<void> deleteManualScoreState({
|
||
required String sessionId,
|
||
required int programIndex,
|
||
required int exerciseIndex,
|
||
required int setIndex,
|
||
required DateTime deletedAt,
|
||
}) async {
|
||
manualScoreStates.removeWhere(
|
||
(state) =>
|
||
state.activeWorkoutSessionId == sessionId &&
|
||
state.programIndex == programIndex &&
|
||
state.exerciseIndex == exerciseIndex &&
|
||
state.setIndex == setIndex,
|
||
);
|
||
}
|
||
|
||
@override
|
||
Future<void> saveSetResult(ActiveSetResult result) async {
|
||
final index = results.indexWhere(
|
||
(saved) =>
|
||
saved.activeWorkoutSessionId == result.activeWorkoutSessionId &&
|
||
saved.programIndex == result.programIndex &&
|
||
saved.exerciseIndex == result.exerciseIndex &&
|
||
saved.setIndex == result.setIndex,
|
||
);
|
||
if (index == -1) {
|
||
results.add(result);
|
||
} else {
|
||
results[index] = result;
|
||
}
|
||
}
|
||
}
|
||
|
||
final class _FakeWorkoutTemplateRepository
|
||
implements WorkoutTemplateRepository {
|
||
@override
|
||
Future<WorkoutTemplate?> findById(String id) async => null;
|
||
|
||
@override
|
||
Future<List<WorkoutTemplate>> listActive() async => const [];
|
||
|
||
@override
|
||
Future<void> replaceComposition(
|
||
WorkoutTemplate template,
|
||
DateTime deletedAt,
|
||
) async {}
|
||
|
||
@override
|
||
Future<void> save(WorkoutTemplate template) async {}
|
||
|
||
@override
|
||
Future<void> saveAll(List<WorkoutTemplate> templates) async {}
|
||
|
||
@override
|
||
Future<void> saveOverride(WorkoutTemplateExerciseOverride override) async {}
|
||
|
||
@override
|
||
Future<void> saveProgram(WorkoutTemplateProgram program) async {}
|
||
}
|
||
|
||
final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||
@override
|
||
Future<void> delete(String id, DateTime deletedAt) async {}
|
||
|
||
@override
|
||
Future<WorkoutHistory?> findById(String id) async => null;
|
||
|
||
@override
|
||
Future<List<WorkoutHistory>> listActive() async => const [];
|
||
|
||
@override
|
||
Future<void> save(WorkoutHistory history) async {}
|
||
|
||
@override
|
||
Future<void> patchHeartRateSummary({
|
||
required String historyId,
|
||
int? minHeartRateBpm,
|
||
required double averageHeartRateBpm,
|
||
required int maxHeartRateBpm,
|
||
double? totalDistanceMeters,
|
||
double? totalCaloriesKcal,
|
||
required DateTime patchedAt,
|
||
}) async {}
|
||
|
||
@override
|
||
Future<void> saveSetResult(WorkoutHistorySetResult result) async {}
|
||
|
||
@override
|
||
Future<void> saveStepResult(WorkoutHistoryStepResult result) async {}
|
||
}
|
||
|
||
final class _FakeProgramRepository implements ProgramRepository {
|
||
@override
|
||
Future<void> replaceExercises(Program program, DateTime deletedAt) async {}
|
||
|
||
@override
|
||
Future<Program?> findById(String id) async => null;
|
||
|
||
@override
|
||
Future<List<Program>> listActive() async => const [];
|
||
|
||
@override
|
||
Future<void> save(Program program) async {}
|
||
|
||
@override
|
||
Future<void> saveExercise(ProgramExercise exercise) async {}
|
||
}
|