Files
GameTime/test/presentation/history_screen_test.dart
Blomios 3b7d904953 feat(statistiques): implémentation frontend statistiques de progression (ticket #82)
Lots F1+F2 : écran progression_screen.dart, intégration home/history/navigation, tests associés (193 tests suite complète, 4 échecs préexistants sans rapport confirmés).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 08:58:16 +02:00

545 lines
16 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 expose le lien secondaire Progression', (tester) async {
var opened = false;
await tester.pumpWidget(
MaterialApp(
home: HistoryListScreen(
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
workoutTemplateUseCases: _workoutTemplateUseCases(),
activeUseCases: _activeUseCases(),
closeUseCase: _closeUseCase(),
onOpenProgression: () => opened = true,
),
),
);
await tester.tap(find.text('Progression'));
await tester.pump();
expect(opened, isTrue);
});
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<ActiveSetTimerState?> findSetTimerState({
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<ActiveSetTimerState>> listSetTimerStates(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> saveSetTimerState(ActiveSetTimerState 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 {}
}