Files
GameTime/test/presentation/history_screen_test.dart
Blomios 8bf4f89276 feat(exercice): plan de séance et historique avec résultats d'étapes (ticket #61)
Affiche le plan de séance et l'historique avec les résultats par
étape sur workout_execution_screen.dart et history_screen.dart.
flutter pub get OK, dart format appliqué (3 fichiers reformatés),
analyze propre (mêmes infos préexistantes), 102/102 tests verts,
build APK debug validé. Dernier ticket UI du chantier "Exercice à
plusieurs étapes" (#54) — il ne reste que la QA finale (#62).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:19:01 +02:00

508 lines
15 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('la liste groupe les séances par période', (tester) async {
final groups = groupHistoriesByPeriod([
_history(id: 'today', startedAt: DateTime.utc(2026, 7, 17, 9)),
_history(id: 'week', startedAt: DateTime.utc(2026, 7, 15, 9)),
_history(id: 'old', startedAt: DateTime.utc(2026, 7, 1, 9)),
], DateTime.utc(2026, 7, 17, 12));
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(children: [for (final key in groups.keys) Text(key)]),
),
),
);
expect(find.text("Aujourd'hui"), findsOneWidget);
expect(find.text('Cette semaine'), findsOneWidget);
expect(find.text('Plus ancien'), findsOneWidget);
});
testWidgets('le détail affiche temps répétitions et score avec unité', (
tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: HistoryDetailScreen(
history: _history(id: 'history-1'),
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
workoutTemplateUseCases: _workoutTemplateUseCases(),
activeUseCases: _activeUseCases(),
closeUseCase: _closeUseCase(),
),
),
);
expect(find.text('Programme jambes'), findsOneWidget);
expect(find.text('Squat'), findsOneWidget);
expect(
find.text('Temps 00:45 · 10 répétitions · Score 80 kg'),
findsOneWidget,
);
});
testWidgets('le détail affiche le score chrono comme temps réalisé', (
tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: HistoryDetailScreen(
history: _history(id: 'history-1', stopwatchScore: true),
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
workoutTemplateUseCases: _workoutTemplateUseCases(),
activeUseCases: _activeUseCases(),
closeUseCase: _closeUseCase(),
),
),
);
expect(
find.text(
'Temps 00:45 · 10 répétitions · '
'Temps réalisé : 00:38.7 / objectif 00:45',
),
findsOneWidget,
);
expect(find.textContaining('Score 38.7'), findsNothing);
});
testWidgets('le détail affiche les résultats détapes par passage', (
tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: HistoryDetailScreen(
history: _history(id: 'history-1', withStepResults: true),
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
workoutTemplateUseCases: _workoutTemplateUseCases(),
activeUseCases: _activeUseCases(),
closeUseCase: _closeUseCase(),
),
),
);
expect(find.text('Étapes réalisées'), findsOneWidget);
expect(find.text('Passage 1'), findsOneWidget);
expect(
find.text(
'Étape 1 · Dribble gauche · Temps 00:10 · Réussites : 4 paniers',
),
findsOneWidget,
);
expect(find.text('Étape 2 · Dribble droite'), findsOneWidget);
expect(find.text('Passée'), findsOneWidget);
});
testWidgets('la suppression demande une confirmation', (tester) async {
final repository = _FakeWorkoutHistoryRepository();
await tester.pumpWidget(
MaterialApp(
home: HistoryDetailScreen(
history: _history(id: 'history-1'),
historyUseCases: _historyUseCases(repository),
workoutTemplateUseCases: _workoutTemplateUseCases(),
activeUseCases: _activeUseCases(),
closeUseCase: _closeUseCase(),
),
),
);
await tester.tap(find.text("Supprimer de l'historique"));
await tester.pumpAndSettle();
expect(repository.deletedIds, isEmpty);
expect(find.text("Supprimer de l'historique ?"), findsOneWidget);
await tester.tap(find.text('Annuler'));
await tester.pumpAndSettle();
expect(repository.deletedIds, isEmpty);
await tester.tap(find.text("Supprimer de l'historique"));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, 'Supprimer'));
await tester.pumpAndSettle();
expect(repository.deletedIds, ['history-1']);
});
}
WorkoutHistory _history({
required String id,
DateTime? startedAt,
bool stopwatchScore = false,
bool withStepResults = false,
}) {
final start = startedAt ?? DateTime.utc(2026, 7, 17, 10);
return WorkoutHistory(
metadata: _metadata(id),
sourceWorkoutTemplateId: 'template-1',
sourceActiveWorkoutSessionId: 'session-1',
nameSnapshot: 'Séance jambes',
startedAt: start,
endedAt: start.add(const Duration(minutes: 30)),
totalActiveMs: 1800000,
completed: true,
historySnapshotJson: jsonEncode({
'sessionId': 'session-1',
'resolvedTemplateSnapshotJson': _resolvedSnapshot(
stopwatchScore: stopwatchScore,
withSteps: withStepResults,
),
'results': [
{
'programSnapshotId': 'program-snapshot-1',
'exerciseSnapshotId': 'exercise-snapshot-1',
'programIndex': 0,
'exerciseIndex': 0,
'setIndex': 0,
'actualTimeMs': 45000,
'actualReps': 10,
if (stopwatchScore) 'actualScoreTimeMs': 38700,
if (!stopwatchScore) 'actualScore': 80,
'scoreLabelSnapshot': 'Charge',
'scoreInputModeSnapshot': stopwatchScore
? ScoreInputMode.stopwatch.name
: ScoreInputMode.manual.name,
if (!stopwatchScore) 'scoreUnitSnapshot': 'kg',
if (stopwatchScore) 'targetScoreTimeMsSnapshot': 45000,
},
],
}),
stepResults: withStepResults
? [
WorkoutHistoryStepResult(
metadata: _metadata('step-result-1'),
workoutHistoryId: id,
programSnapshotId: 'program-snapshot-1',
exerciseSnapshotId: 'exercise-snapshot-1',
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
passageIndex: 0,
stepIndex: 0,
stepSnapshotId: 'step-1',
stepNameSnapshot: 'Dribble gauche',
stepTypeSnapshot: ExerciseStepType.time,
targetValueSnapshot: 10,
hasScoreSnapshot: true,
scoreInputModeSnapshot: ScoreInputMode.manual,
scoreLabelSnapshot: 'Réussites',
scoreUnitSnapshot: 'paniers',
targetScoreSnapshot: 3,
status: SetResultStatus.completed,
actualTimeMs: 10000,
actualScore: 4,
),
WorkoutHistoryStepResult(
metadata: _metadata('step-result-2'),
workoutHistoryId: id,
programSnapshotId: 'program-snapshot-1',
exerciseSnapshotId: 'exercise-snapshot-1',
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
passageIndex: 0,
stepIndex: 1,
stepSnapshotId: 'step-2',
stepNameSnapshot: 'Dribble droite',
stepTypeSnapshot: ExerciseStepType.reps,
targetValueSnapshot: 8,
hasScoreSnapshot: false,
status: SetResultStatus.skipped,
),
]
: const [],
);
}
String _resolvedSnapshot({
bool stopwatchScore = false,
bool withSteps = false,
}) {
return jsonEncode({
'name': 'Séance jambes',
'programs': [
{
'id': 'program-snapshot-1',
'programNameSnapshot': 'Programme jambes',
'programSnapshotJson': jsonEncode({
'exercises': [
{
'id': 'exercise-snapshot-1',
'exerciseNameSnapshot': 'Squat',
'setsCount': 1,
'timeEnabled': true,
'repsEnabled': true,
'scoreEnabled': true,
'targetTimeSeconds': 45,
'targetReps': 10,
if (stopwatchScore) 'targetScoreTimeMs': 45000,
if (!stopwatchScore) 'targetScore': 80,
'scoreInputModeSnapshot': stopwatchScore
? ScoreInputMode.stopwatch.name
: ScoreInputMode.manual.name,
'scoreLabelSnapshot': 'Charge',
if (!stopwatchScore) 'scoreUnitSnapshot': 'kg',
'restSecondsOverride': 0,
if (withSteps)
'exerciseStepsSnapshot': [
{
'id': 'step-1',
'position': 0,
'name': 'Dribble gauche',
'type': ExerciseStepType.time.name,
'defaultTargetValue': 10,
'hasScore': true,
'scoreInputMode': ScoreInputMode.manual.name,
'scoreLabel': 'Réussites',
'scoreUnit': 'paniers',
'defaultTargetScore': 3,
},
{
'id': 'step-2',
'position': 1,
'name': 'Dribble droite',
'type': ExerciseStepType.reps.name,
'defaultTargetValue': 8,
'hasScore': false,
},
],
},
],
}),
},
],
'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',
);
}
WorkoutHistoryUseCases _historyUseCases(
_FakeWorkoutHistoryRepository repository,
) {
return WorkoutHistoryUseCases(
repository: repository,
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
);
}
WorkoutTemplateUseCases _workoutTemplateUseCases() {
return WorkoutTemplateUseCases(
templateRepository: _FakeWorkoutTemplateRepository(),
programRepository: _FakeProgramRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}
ActiveWorkoutSessionUseCases _activeUseCases() {
return ActiveWorkoutSessionUseCases(
sessionRepository: _FakeActiveSessionRepository(),
templateRepository: _FakeWorkoutTemplateRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}
CloseWorkoutSessionUseCase _closeUseCase() {
return CloseWorkoutSessionUseCase(
sessionRepository: _FakeActiveSessionRepository(),
historyRepository: _FakeWorkoutHistoryRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
ids: _FakeIds(),
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 _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
final deletedIds = <String>[];
@override
Future<void> delete(String id, DateTime deletedAt) async {
deletedIds.add(id);
}
@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 {}
@override
Future<void> saveStepResult(WorkoutHistoryStepResult result) async {}
}
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 _FakeProgramRepository implements ProgramRepository {
@override
Future<Program?> findById(String id) async => null;
@override
Future<List<Program>> listActive() async => const [];
@override
Future<void> replaceExercises(Program program, DateTime deletedAt) async {}
@override
Future<void> save(Program program) async {}
@override
Future<void> saveExercise(ProgramExercise exercise) async {}
}
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
@override
Future<ActiveWorkoutSession?> findById(String id) async => null;
@override
Future<ActiveWorkoutSession?> findOpen() async => null;
@override
Future<ActiveRestState?> findRestStateById(String id) async => null;
@override
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
}) async => null;
@override
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
}) async => null;
@override
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
return const [];
}
@override
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
String sessionId,
) async {
return const [];
}
@override
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
return const [];
}
@override
Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
String sessionId,
) async {
return const [];
}
@override
Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
String sessionId,
) async {
return const [];
}
@override
Future<void> save(ActiveWorkoutSession session) async {}
@override
Future<void> saveRestState(ActiveRestState restState) async {}
@override
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {}
@override
Future<void> saveExerciseStepProgressState(
ActiveExerciseStepProgressState state,
) async {}
@override
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {}
@override
Future<void> deleteScoreStopwatchState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
required DateTime deletedAt,
}) async {}
@override
Future<void> saveSetResult(ActiveSetResult result) async {}
}