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>
603 lines
18 KiB
Dart
603 lines
18 KiB
Dart
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 {}
|
||
}
|