Files
GameTime/test/presentation/progression_screen_test.dart
Blomios 917777e18b chore(wip): consolidation intermédiaire multi-tickets (sprints Statistiques, UI, Bug resolution, Serveur-client)
Regroupe l'état de travail en cours réalisé dans un même worktree sur
plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164),
mélangeant des tickets QA et inProgress. Ne constitue pas une feature
terminée : commit de sauvegarde avant triage/split par ticket en
branches feature/* dédiées. Exclut les dossiers d'environnement de
build locaux et le heap dump parasite (.gitignore mis à jour).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 16:48:54 +02:00

642 lines
19 KiB
Dart
Raw Permalink 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('Progression affiche la période par défaut et létat vide global', (
tester,
) async {
final repository = _FakeProgressionStatsRepository();
await tester.pumpWidget(_app(repository: repository));
await tester.pumpAndSettle();
expect(find.text('Progression'), findsOneWidget);
expect(find.text('4 semaines'), findsOneWidget);
expect(find.text('3 mois'), findsOneWidget);
expect(find.text('Tout'), findsOneWidget);
expect(find.text('Aucune progression à afficher'), findsOneWidget);
expect(
find.text(
'Termine une séance pour voir ton volume, ton rythme et tes résultats évoluer ici.',
),
findsOneWidget,
);
expect(find.text('Voir les séances'), findsOneWidget);
expect(repository.periods, [ProgressionPeriod.fourWeeks]);
});
testWidgets('Progression affiche Volume et Régularité', (tester) async {
final repository = _FakeProgressionStatsRepository()
..overview = ProgressionGlobalStatsData(
completedSessionCount: 8,
totalActiveMs: 20400000,
activeWeekStarts: [
DateTime.utc(2026, 7, 6),
DateTime.utc(2026, 7, 13),
DateTime.utc(2026, 7, 20),
],
hasAnyCompletedHistory: true,
firstCompletedSessionAt: DateTime.utc(2026, 7, 1),
);
await tester.pumpWidget(_app(repository: repository));
await tester.pumpAndSettle();
expect(find.text('Volume'), findsOneWidget);
expect(find.text('Séances'), findsOneWidget);
expect(find.text('8'), findsOneWidget);
expect(find.text('Temps total'), findsOneWidget);
expect(find.text('5 h 40'), findsOneWidget);
expect(find.text('Régularité'), findsOneWidget);
expect(find.text('Rythme récent'), findsOneWidget);
expect(find.text('3 / 4 semaines actives'), findsOneWidget);
expect(
find.text('Une semaine active contient au moins une séance terminée.'),
findsOneWidget,
);
});
testWidgets('Par exercice affiche un exercice archivé et un point unique', (
tester,
) async {
final repository = _FakeProgressionStatsRepository()
..overview = ProgressionGlobalStatsData(
completedSessionCount: 1,
totalActiveMs: 1800000,
activeWeekStarts: [DateTime.utc(2026, 7, 13)],
hasAnyCompletedHistory: true,
)
..exerciseOptions = [
ProgressionExerciseOptionData(
exerciseKey: 'shoot',
nameSnapshot: 'Shoot ligne',
isArchived: true,
lastPerformedAt: DateTime.utc(2026, 7, 17),
),
]
..measuresByExercise = const {
'shoot': [
ProgressionMeasureOptionData(
measure: ProgressionMeasure.reps,
label: 'Répétitions',
lowerIsBetter: false,
),
],
}
..seriesByExerciseMeasure = {
'shoot:${ProgressionMeasure.reps.name}': ProgressionExerciseSeriesData(
exerciseKey: 'shoot',
exerciseNameSnapshot: 'Shoot ligne',
measure: ProgressionMeasureOptionData(
measure: ProgressionMeasure.reps,
label: 'Répétitions',
lowerIsBetter: false,
),
points: [
ProgressionPointData(
workoutHistoryId: 'history-1',
workoutNameSnapshot: 'Prépa match',
startedAt: DateTime.utc(2026, 7, 17),
value: 45,
rawValue: 45,
),
],
hasAnyAllTimeData: true,
hasAnyCompletedExerciseResult: true,
),
};
await tester.pumpWidget(_app(repository: repository));
await tester.pumpAndSettle();
expect(find.text('Par exercice'), findsOneWidget);
expect(find.text('Choisir un exercice'), findsOneWidget);
expect(find.text('Shoot ligne'), findsOneWidget);
expect(find.text('Exercice archivé'), findsOneWidget);
expect(find.text('Encore un point de départ'), findsOneWidget);
expect(
find.text(
'Termine cet exercice dans une autre séance pour voir son évolution.',
),
findsOneWidget,
);
expect(find.text('Dernière séance : 45 répétitions'), findsOneWidget);
});
testWidgets('Par exercice affiche Temps réalisé et Plus bas = mieux', (
tester,
) async {
final repository = _progressionRepositoryWithSeries(
measure: const ProgressionMeasureOptionData(
measure: ProgressionMeasure.stopwatchScore,
label: 'Temps réalisé',
lowerIsBetter: true,
),
rawValues: [45100, 42800],
values: [45100, 42800],
);
await tester.pumpWidget(_app(repository: repository));
await tester.pumpAndSettle();
expect(find.text('Temps réalisé'), findsOneWidget);
expect(find.text('Plus bas = mieux'), findsOneWidget);
expect(find.text('Meilleur : 00:42.8'), findsOneWidget);
expect(find.text('Dernier : 00:42.8'), findsOneWidget);
});
testWidgets('tap sur un point affiche le détail et ouvre la séance', (
tester,
) async {
final historyRepository = _FakeWorkoutHistoryRepository()
..histories['history-2'] = _history(id: 'history-2');
final repository = _progressionRepositoryWithSeries(
measure: const ProgressionMeasureOptionData(
measure: ProgressionMeasure.manualScore,
label: 'Score',
scoreLabel: 'Réussites',
scoreUnit: 'paniers',
lowerIsBetter: false,
),
rawValues: [12.0, 14.0],
values: [12, 14],
);
await tester.pumpWidget(
_app(repository: repository, historyRepository: historyRepository),
);
await tester.pumpAndSettle();
final pointFinder = find.byKey(const ValueKey('progression-point-1'));
await tester.ensureVisible(pointFinder);
await tester.pumpAndSettle();
await tester.tap(pointFinder);
await tester.pumpAndSettle();
expect(find.text('12 juillet'), findsOneWidget);
expect(find.text('Séance : Prépa match'), findsOneWidget);
expect(find.text('Meilleur score : 14 paniers'), findsOneWidget);
await tester.tap(find.text('Voir la séance'));
await tester.pumpAndSettle();
expect(historyRepository.requestedIds, ['history-2']);
expect(find.text('Séance jambes'), findsWidgets);
});
}
Widget _app({
required _FakeProgressionStatsRepository repository,
_FakeWorkoutHistoryRepository? historyRepository,
}) {
final clock = _FakeClock(DateTime.utc(2026, 7, 22, 12));
final activeRepository = _FakeActiveSessionRepository();
final resolvedHistoryRepository =
historyRepository ?? _FakeWorkoutHistoryRepository();
return MaterialApp(
theme: courtBlazerLightTheme(),
home: ProgressionScreen(
progressionStatsUseCase: ProgressionStatsUseCase(
repository: repository,
clock: clock,
),
historyUseCases: WorkoutHistoryUseCases(
repository: resolvedHistoryRepository,
clock: clock,
),
workoutTemplateUseCases: WorkoutTemplateUseCases(
templateRepository: _FakeWorkoutTemplateRepository(),
programRepository: _FakeProgramRepository(),
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
),
activeUseCases: ActiveWorkoutSessionUseCases(
sessionRepository: activeRepository,
templateRepository: _FakeWorkoutTemplateRepository(),
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
),
closeUseCase: CloseWorkoutSessionUseCase(
sessionRepository: activeRepository,
historyRepository: resolvedHistoryRepository,
clock: clock,
ids: _FakeIds(),
originDeviceId: 'device-1',
),
),
);
}
_FakeProgressionStatsRepository _progressionRepositoryWithSeries({
required ProgressionMeasureOptionData measure,
required List<Object> rawValues,
required List<double> values,
}) {
return _FakeProgressionStatsRepository()
..overview = ProgressionGlobalStatsData(
completedSessionCount: 2,
totalActiveMs: 3600000,
activeWeekStarts: [DateTime.utc(2026, 7, 6), DateTime.utc(2026, 7, 13)],
hasAnyCompletedHistory: true,
)
..exerciseOptions = [
ProgressionExerciseOptionData(
exerciseKey: 'shoot',
nameSnapshot: 'Shoot ligne',
isArchived: false,
lastPerformedAt: DateTime.utc(2026, 7, 17),
),
]
..measuresByExercise = {
'shoot': [measure],
}
..seriesByExerciseMeasure = {
'shoot:${measure.measure.name}': ProgressionExerciseSeriesData(
exerciseKey: 'shoot',
exerciseNameSnapshot: 'Shoot ligne',
measure: measure,
points: [
ProgressionPointData(
workoutHistoryId: 'history-1',
workoutNameSnapshot: 'Prépa match',
startedAt: DateTime.utc(2026, 7, 5),
value: values[0],
rawValue: rawValues[0],
),
ProgressionPointData(
workoutHistoryId: 'history-2',
workoutNameSnapshot: 'Prépa match',
startedAt: DateTime.utc(2026, 7, 12),
value: values[1],
rawValue: rawValues[1],
),
],
hasAnyAllTimeData: true,
hasAnyCompletedExerciseResult: true,
),
};
}
final class _FakeProgressionStatsRepository
implements ProgressionStatsRepository {
ProgressionGlobalStatsData overview = const ProgressionGlobalStatsData(
completedSessionCount: 0,
totalActiveMs: 0,
activeWeekStarts: [],
hasAnyCompletedHistory: false,
);
List<ProgressionExerciseOptionData> exerciseOptions = const [];
Map<String, List<ProgressionMeasureOptionData>> measuresByExercise = const {};
Map<String, ProgressionExerciseSeriesData> seriesByExerciseMeasure = const {};
final periods = <ProgressionPeriod>[];
@override
Future<ProgressionGlobalStatsData> readGlobalStats(
ProgressionDateRange range,
) async {
periods.add(
range.startedAt == null
? ProgressionPeriod.all
: range.startedAt!.isAfter(DateTime.utc(2026, 5))
? ProgressionPeriod.fourWeeks
: ProgressionPeriod.threeMonths,
);
return overview;
}
@override
Future<List<ProgressionExerciseOptionData>> listExerciseOptions(
ProgressionDateRange range,
) async {
return exerciseOptions;
}
@override
Future<List<ProgressionMeasureOptionData>> listMeasureOptions({
required ProgressionDateRange range,
required String exerciseKey,
}) async {
return measuresByExercise[exerciseKey] ?? const [];
}
@override
Future<ProgressionExerciseSeriesData> readExerciseSeries({
required ProgressionDateRange range,
required String exerciseKey,
required ProgressionMeasure measure,
}) async {
return seriesByExerciseMeasure['$exerciseKey:${measure.name}'] ??
ProgressionExerciseSeriesData(
exerciseKey: exerciseKey,
exerciseNameSnapshot: exerciseKey,
measure: ProgressionMeasureOptionData(
measure: measure,
label: 'Score',
lowerIsBetter: false,
),
points: const [],
hasAnyAllTimeData: false,
hasAnyCompletedExerciseResult: false,
);
}
}
WorkoutHistory _history({required String id}) {
final start = DateTime.utc(2026, 7, 12, 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': 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,
'targetScore': 80,
'scoreInputModeSnapshot': ScoreInputMode.manual.name,
'scoreLabelSnapshot': 'Charge',
'scoreUnitSnapshot': 'kg',
'restSecondsOverride': 0,
},
],
}),
},
],
'overrides': const [],
}),
'results': [
{
'programSnapshotId': 'program-snapshot-1',
'exerciseSnapshotId': 'exercise-snapshot-1',
'programIndex': 0,
'exerciseIndex': 0,
'setIndex': 0,
'actualTimeMs': 45000,
'actualReps': 10,
'actualScore': 80,
'scoreLabelSnapshot': 'Charge',
'scoreInputModeSnapshot': ScoreInputMode.manual.name,
'scoreUnitSnapshot': 'kg',
},
],
}),
);
}
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);
final 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 histories = <String, WorkoutHistory>{};
final requestedIds = <String>[];
@override
Future<void> delete(String id, DateTime deletedAt) async {}
@override
Future<WorkoutHistory?> findById(String id) async {
requestedIds.add(id);
return histories[id];
}
@override
Future<List<WorkoutHistory>> listActive() async => histories.values.toList();
@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 _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 _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<void> deleteScoreStopwatchState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
required DateTime deletedAt,
}) async {}
@override
Future<ActiveWorkoutSession?> findById(String id) async => null;
@override
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
}) 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<ActiveManualScoreState?> findManualScoreState({
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<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
String sessionId,
) async => const [];
@override
Future<List<ActiveExerciseStepResult>> listExerciseStepResults(
String sessionId,
) async => const [];
@override
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
return const [];
}
@override
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
String sessionId,
) async => const [];
@override
Future<List<ActiveManualScoreState>> listManualScoreStates(
String sessionId,
) async => const [];
@override
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
return const [];
}
@override
Future<List<ActiveSetTimerState>> listSetTimerStates(String sessionId) async {
return const [];
}
@override
Future<void> save(ActiveWorkoutSession session) async {}
@override
Future<void> saveExerciseStepProgressState(
ActiveExerciseStepProgressState state,
) async {}
@override
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {}
@override
Future<void> saveRestState(ActiveRestState restState) async {}
@override
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {}
@override
Future<void> saveManualScoreState(ActiveManualScoreState state) async {}
@override
Future<void> saveSetResult(ActiveSetResult result) async {}
@override
Future<void> saveSetTimerState(ActiveSetTimerState state) async {}
@override
Future<void> deleteManualScoreState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
required DateTime deletedAt,
}) async {}
}