feat(execution): refonte de l'UI de l'écran de séance (ticket #93)
Refonte de workout_execution_screen.dart cadrée UX+Architect ; résout au passage la dette de tests du ticket #90 (3 échecs préexistants désormais verts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -42,7 +42,7 @@ void main() {
|
||||
|
||||
expect(find.text('Squat'), findsOneWidget);
|
||||
expect(find.text('Temps de série : 00:45'), findsOneWidget);
|
||||
expect(find.text('Démarrer l’exercice'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, 'Démarrer'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('les répétitions sont initialisées avec la cible', (
|
||||
@ -81,6 +81,73 @@ void main() {
|
||||
expect(find.text('8'), 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('Temps 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 {
|
||||
@ -379,7 +446,7 @@ void main() {
|
||||
expect(find.text('Chrono score'), findsOneWidget);
|
||||
expect(find.text('00:00.0'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Démarrer'));
|
||||
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));
|
||||
@ -447,13 +514,10 @@ void main() {
|
||||
|
||||
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 l’exercice'),
|
||||
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);
|
||||
});
|
||||
@ -495,7 +559,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Démarrer l’exercice'));
|
||||
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));
|
||||
@ -662,7 +726,7 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('Démarrer l’exercice'));
|
||||
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));
|
||||
@ -713,12 +777,12 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Programme 1/1 · Exercice 1/1'), findsOneWidget);
|
||||
expect(find.text('Série 2/3'), findsOneWidget);
|
||||
expect(find.text('Programme jambes'), findsOneWidget);
|
||||
expect(find.text('2 / 3'), findsOneWidget);
|
||||
expect(find.text('Squat'), findsOneWidget);
|
||||
expect(find.text('Temps de série : 00:30'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Démarrer l’exercice'));
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Démarrer').first);
|
||||
await tester.pump();
|
||||
|
||||
for (var i = 0; i < 2; i++) {
|
||||
@ -738,8 +802,8 @@ void main() {
|
||||
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('Série 3/3'), findsOneWidget);
|
||||
expect(find.text('Programme jambes'), findsOneWidget);
|
||||
expect(find.text('3 / 3'), findsOneWidget);
|
||||
expect(find.text('5'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
@ -1025,8 +1089,8 @@ void main() {
|
||||
await tester.tap(find.byTooltip('Fermer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Programme 1/1 · Exercice 1/1'), findsOneWidget);
|
||||
expect(find.text('Série 3/4'), findsOneWidget);
|
||||
expect(find.text('Programme jambes'), findsOneWidget);
|
||||
expect(find.text('3 / 4'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
@ -1071,8 +1135,8 @@ void main() {
|
||||
|
||||
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('Série 2/3'), findsOneWidget);
|
||||
expect(find.text('Programme jambes'), findsOneWidget);
|
||||
expect(find.text('2 / 3'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
@ -1127,6 +1191,72 @@ void main() {
|
||||
expect(find.text('00:10'), findsOneWidget);
|
||||
});
|
||||
|
||||
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));
|
||||
@ -1215,7 +1345,7 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('00:05'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Démarrer la séquence'));
|
||||
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));
|
||||
@ -1274,13 +1404,10 @@ void main() {
|
||||
|
||||
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 l’exercice'),
|
||||
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);
|
||||
},
|
||||
@ -1330,7 +1457,7 @@ void main() {
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Démarrer la séquence'));
|
||||
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));
|
||||
@ -1392,9 +1519,7 @@ void main() {
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(
|
||||
find.widgetWithText(FilledButton, 'Démarrer la séquence'),
|
||||
);
|
||||
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));
|
||||
@ -1404,15 +1529,15 @@ void main() {
|
||||
expect(find.text('ÉTAPE 2 / 2'), findsOneWidget);
|
||||
expect(find.text('Droite'), findsOneWidget);
|
||||
expect(find.text('00:02'), findsOneWidget);
|
||||
expect(find.text('Chrono suivant prêt'), findsOneWidget);
|
||||
expect(find.text('Démarrer le chrono'), 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, 'Démarrer le chrono'));
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Lancer'));
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
@ -1466,7 +1591,7 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Passage 2 / 2'), findsOneWidget);
|
||||
expect(find.text('Passages réalisés : 1 / 2'), findsOneWidget);
|
||||
expect(find.textContaining('Passages réalisés'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('terminer la série reste utilisable avant la fin de séquence', (
|
||||
@ -1678,18 +1803,23 @@ String _sessionSnapshot({
|
||||
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': 'Séance jambes',
|
||||
'name': workoutName,
|
||||
'programs': [
|
||||
{
|
||||
'id': 'template-program-1',
|
||||
'programNameSnapshot': 'Programme jambes',
|
||||
'programNameSnapshot': programName,
|
||||
'programSnapshotJson': jsonEncode({
|
||||
'exercises': [
|
||||
{
|
||||
'id': 'exercise-1',
|
||||
'exerciseNameSnapshot': 'Squat',
|
||||
'sourceExerciseId': sourceExerciseId,
|
||||
'exerciseNameSnapshot': exerciseName,
|
||||
'setsCount': setsCount,
|
||||
'timeEnabled': timeEnabled,
|
||||
'repsEnabled': repsEnabled,
|
||||
@ -1794,6 +1924,37 @@ final class _FakeStepAudioCuePlayer implements ExerciseStepAudioCuePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
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>[];
|
||||
|
||||
Reference in New Issue
Block a user