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>
This commit is contained in:
@ -16,6 +16,7 @@ final class HistoryListScreen extends StatefulWidget {
|
|||||||
this.mediaUseCases,
|
this.mediaUseCases,
|
||||||
this.stepUseCases,
|
this.stepUseCases,
|
||||||
this.performanceReferenceUseCase,
|
this.performanceReferenceUseCase,
|
||||||
|
this.onOpenProgression,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ final class HistoryListScreen extends StatefulWidget {
|
|||||||
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final MediaUseCases? mediaUseCases;
|
final MediaUseCases? mediaUseCases;
|
||||||
|
final VoidCallback? onOpenProgression;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<HistoryListScreen> createState() => _HistoryListScreenState();
|
State<HistoryListScreen> createState() => _HistoryListScreenState();
|
||||||
@ -43,7 +45,16 @@ final class _HistoryListScreenState extends State<HistoryListScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Historique')),
|
appBar: AppBar(
|
||||||
|
title: const Text('Historique'),
|
||||||
|
actions: [
|
||||||
|
if (widget.onOpenProgression != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: widget.onOpenProgression,
|
||||||
|
child: const Text('Progression'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
body: FutureBuilder<List<WorkoutHistory>>(
|
body: FutureBuilder<List<WorkoutHistory>>(
|
||||||
future: _histories,
|
future: _histories,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import '../domain/domain.dart';
|
|||||||
import 'exercise_library_screen.dart';
|
import 'exercise_library_screen.dart';
|
||||||
import 'history_screen.dart';
|
import 'history_screen.dart';
|
||||||
import 'profile_screen.dart';
|
import 'profile_screen.dart';
|
||||||
|
import 'progression_screen.dart';
|
||||||
import 'program_screen.dart';
|
import 'program_screen.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
import 'workout_execution_screen.dart';
|
import 'workout_execution_screen.dart';
|
||||||
@ -185,10 +186,20 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||||
|
onOpenProgression: () => _openProgression(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.show_chart),
|
||||||
|
title: const Text('Progression'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Volume, régularité et évolution par exercice',
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => _openProgression(context),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: FutureBuilder<UserAccountSession?>(
|
leading: FutureBuilder<UserAccountSession?>(
|
||||||
future: widget.bootstrap.authUseCases.currentSession(),
|
future: widget.bootstrap.authUseCases.currentSession(),
|
||||||
@ -225,6 +236,24 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _openProgression(BuildContext context) {
|
||||||
|
return Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ProgressionScreen(
|
||||||
|
progressionStatsUseCase: widget.bootstrap.progressionStatsUseCase,
|
||||||
|
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||||
|
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
|
||||||
|
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||||
|
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
||||||
|
performanceReferenceUseCase:
|
||||||
|
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
||||||
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
|
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _resume(ActiveWorkoutSession session) async {
|
Future<void> _resume(ActiveWorkoutSession session) async {
|
||||||
await Navigator.of(context).push(
|
await Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export 'exercise_library_screen.dart';
|
|||||||
export 'history_screen.dart';
|
export 'history_screen.dart';
|
||||||
export 'home_screen.dart';
|
export 'home_screen.dart';
|
||||||
export 'profile_screen.dart';
|
export 'profile_screen.dart';
|
||||||
|
export 'progression_screen.dart';
|
||||||
export 'program_screen.dart';
|
export 'program_screen.dart';
|
||||||
export 'share_inbox_screen.dart';
|
export 'share_inbox_screen.dart';
|
||||||
export 'share_screen.dart';
|
export 'share_screen.dart';
|
||||||
|
|||||||
1055
lib/presentation/progression_screen.dart
Normal file
1055
lib/presentation/progression_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,27 @@ import 'package:gametime/domain/domain.dart';
|
|||||||
import 'package:gametime/presentation/presentation.dart';
|
import 'package:gametime/presentation/presentation.dart';
|
||||||
|
|
||||||
void main() {
|
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 {
|
testWidgets('la liste groupe les séances par période', (tester) async {
|
||||||
final groups = groupHistoriesByPeriod([
|
final groups = groupHistoriesByPeriod([
|
||||||
_history(id: 'today', startedAt: DateTime.utc(2026, 7, 17, 9)),
|
_history(id: 'today', startedAt: DateTime.utc(2026, 7, 17, 9)),
|
||||||
|
|||||||
@ -9,6 +9,31 @@ import 'package:gametime/domain/domain.dart';
|
|||||||
import 'package:gametime/presentation/home_screen.dart';
|
import 'package:gametime/presentation/home_screen.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
testWidgets('l’entrée Progression est présente après Historique', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
navigatorObservers: [homeRouteObserver],
|
||||||
|
home: HomeScreen(
|
||||||
|
bootstrap: _FakeBootstrap(_FakeActiveSessionRepository()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Historique'), findsOneWidget);
|
||||||
|
expect(find.text('Progression'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.text('Volume, régularité et évolution par exercice'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tester.getTopLeft(find.text('Progression')).dy,
|
||||||
|
greaterThan(tester.getTopLeft(find.text('Historique')).dy),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('l’entrée Profil est présente sur l’accueil', (tester) async {
|
testWidgets('l’entrée Profil est présente sur l’accueil', (tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
|||||||
602
test/presentation/progression_screen_test.dart
Normal file
602
test/presentation/progression_screen_test.dart
Normal file
@ -0,0 +1,602 @@
|
|||||||
|
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> 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<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<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<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> saveSetResult(ActiveSetResult result) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveSetTimerState(ActiveSetTimerState state) async {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user