Files
GameTime/test/presentation/workout_execution_screen_test.dart
Blomios cdcb0beead feat(exécution): chrono du score chronométré pendant la séance (ticket #27)
Ajoute le chrono du score chronométré (fondation domain/config des
tickets #25/#26) sur workout_execution_screen.dart, avec les use
cases associés. flutter analyze propre, 75/75 tests verts, build APK
debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:34:36 +02:00

1091 lines
34 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gametime/application/application.dart';
import 'package:gametime/domain/domain.dart';
import 'package:gametime/presentation/presentation.dart';
void main() {
testWidgets('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('00:45'), findsOneWidget);
});
testWidgets('les mesures cumulées respectent la hiérarchie visuelle', (
tester,
) async {
final scoreController = TextEditingController();
addTearDown(scoreController.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,
onRepsChanged: (_) {},
),
),
),
);
final timeY = tester.getTopLeft(find.text('Temps')).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 saffiche 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 lexercice'), findsNothing);
});
testWidgets('le bouton médias ouvre la sheet si médias présents', (
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(
imageMediaIds: const ['image-1'],
videoMediaId: 'video-1',
),
);
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(
_FakeActiveSessionRepository(),
clock,
),
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
expect(find.byTooltip('Voir les médias de lexercice'), findsOneWidget);
await tester.tap(find.text('Voir médias'));
await tester.pumpAndSettle();
expect(find.text('Médias de lexercice'), findsOneWidget);
expect(find.text('Images'), findsOneWidget);
expect(find.text('Vidéo'), findsOneWidget);
expect(find.text('1/1'), findsOneWidget);
expect(find.text('Image 1'), findsOneWidget);
});
testWidgets('la galerie médias navigue entre les images', (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(
imageMediaIds: const ['image-1', 'image-2', 'image-3'],
),
);
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(
_FakeActiveSessionRepository(),
clock,
),
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
await tester.tap(find.text('Voir médias'));
await tester.pumpAndSettle();
expect(find.text('1/3'), findsOneWidget);
await tester.drag(find.byType(PageView), const Offset(-400, 0));
await tester.pumpAndSettle();
expect(find.text('2/3'), findsOneWidget);
expect(find.text('Image 2'), findsOneWidget);
});
testWidgets('le chrono score démarre et sarrê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(
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.text('Démarrer'));
await tester.pump();
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.text('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(
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.text('Démarrer le chrono'), findsOneWidget);
expect(find.text('Terminer sans chrono'), findsOneWidget);
expect(repository.results, isEmpty);
});
testWidgets('terminer avec chrono en cours larrê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,
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('Démarrer'));
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('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.pump(const Duration(seconds: 2));
await tester.tap(find.text('Terminer la série'));
await tester.pump();
expect(repository.results.single.actualTimeMs, greaterThan(0));
});
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 1/1 · Exercice 1/1'), findsOneWidget);
expect(find.text('SÉRIE'), findsOneWidget);
expect(find.text('2 / 3'), findsOneWidget);
expect(find.text('30 s'), findsOneWidget);
for (var i = 0; i < 3; i++) {
await tester.tap(find.byIcon(Icons.add));
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 1/1 · Exercice 1/1'), findsOneWidget);
expect(find.text('3 / 3'), findsOneWidget);
},
);
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(
'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 1/1 · Exercice 1/1'), 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 1/1 · Exercice 1/1'), findsOneWidget);
expect(find.text('2 / 3'), findsOneWidget);
},
);
}
Finder _textFieldWithLabel(String label) {
return find.byWidgetPredicate(
(widget) => widget is TextField && widget.decoration?.labelText == label,
description: 'TextField with label "$label"',
);
}
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',
);
}
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,
int targetTimeSeconds = 45,
int targetReps = 10,
double targetScore = 80,
String scoreUnit = 'kg',
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
List<String> imageMediaIds = const [],
String? videoMediaId,
}) {
return jsonEncode({
'name': 'Séance jambes',
'programs': [
{
'id': 'template-program-1',
'programNameSnapshot': 'Programme jambes',
'programSnapshotJson': jsonEncode({
'exercises': [
{
'id': 'exercise-1',
'exerciseNameSnapshot': 'Squat',
'setsCount': setsCount,
'timeEnabled': true,
'repsEnabled': true,
'scoreEnabled': true,
'targetTimeSeconds': targetTimeSeconds,
'targetReps': targetReps,
'targetScore': targetScore,
'scoreInputModeSnapshot': scoreInputMode.name,
'scoreLabelSnapshot': 'Score',
'scoreUnitSnapshot': scoreUnit,
'restSecondsOverride': restSeconds,
'exerciseImageMediaIdsSnapshot': imageMediaIds,
'imageMediaIdsSnapshot': imageMediaIds,
'exerciseVideoMediaIdSnapshot': videoMediaId,
'videoMediaIdSnapshot': videoMediaId,
},
],
}),
},
],
'overrides': const [],
});
}
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 _FakeActiveSessionRepository implements ActiveSessionRepository {
ActiveWorkoutSession? session;
final results = <ActiveSetResult>[];
final restStates = <ActiveRestState>[];
final scoreStopwatchStates = <ActiveScoreStopwatchState>[];
@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<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<ActiveSetResult>> listSetResults(String sessionId) async {
return results;
}
@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> 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> 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> 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> saveSetResult(WorkoutHistorySetResult 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 {}
}