feat(ui): écran Historique des séances
Ajoute l'écran d'historique des séances (presentation/history_screen.dart), avec les ajustements application/domaine/repositories nécessaires et la navigation depuis l'accueil. Complète le dernier écran du périmètre UI. flutter analyze propre, 18/18 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
305
test/presentation/history_screen_test.dart
Normal file
305
test/presentation/history_screen_test.dart
Normal file
@ -0,0 +1,305 @@
|
||||
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('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}) {
|
||||
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(),
|
||||
'results': [
|
||||
{
|
||||
'programSnapshotId': 'program-snapshot-1',
|
||||
'exerciseSnapshotId': 'exercise-snapshot-1',
|
||||
'programIndex': 0,
|
||||
'exerciseIndex': 0,
|
||||
'setIndex': 0,
|
||||
'actualTimeMs': 45000,
|
||||
'actualReps': 10,
|
||||
'actualScore': 80,
|
||||
'scoreLabelSnapshot': 'Charge',
|
||||
'scoreUnitSnapshot': 'kg',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
String _resolvedSnapshot() {
|
||||
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,
|
||||
'targetScore': 80,
|
||||
'scoreLabelSnapshot': 'Charge',
|
||||
'scoreUnitSnapshot': 'kg',
|
||||
'restSecondsOverride': 0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
'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 {}
|
||||
}
|
||||
|
||||
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<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(ActiveWorkoutSession session) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveRestState(ActiveRestState restState) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveSetResult(ActiveSetResult result) async {}
|
||||
}
|
||||
@ -33,6 +33,8 @@ void main() {
|
||||
clock,
|
||||
),
|
||||
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
|
||||
historyUseCases: _historyUseCases(clock),
|
||||
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -141,6 +143,23 @@ CloseWorkoutSessionUseCase _closeUseCase(
|
||||
);
|
||||
}
|
||||
|
||||
WorkoutHistoryUseCases _historyUseCases(_FakeClock clock) {
|
||||
return WorkoutHistoryUseCases(
|
||||
repository: _FakeWorkoutHistoryRepository(),
|
||||
clock: clock,
|
||||
);
|
||||
}
|
||||
|
||||
WorkoutTemplateUseCases _workoutTemplateUseCases() {
|
||||
return WorkoutTemplateUseCases(
|
||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||
programRepository: _FakeProgramRepository(),
|
||||
clock: _FakeClock(DateTime.utc(2026, 7, 17)),
|
||||
ids: _FakeIds(),
|
||||
originDeviceId: 'device-1',
|
||||
);
|
||||
}
|
||||
|
||||
String _sessionSnapshot({int setsCount = 3, int restSeconds = 0}) {
|
||||
return jsonEncode({
|
||||
'name': 'Séance jambes',
|
||||
@ -260,9 +279,35 @@ final class _FakeWorkoutTemplateRepository
|
||||
}
|
||||
|
||||
final class _FakeWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||
@override
|
||||
Future<void> delete(String id, DateTime deletedAt) async {}
|
||||
|
||||
@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 {}
|
||||
}
|
||||
|
||||
final class _FakeProgramRepository implements ProgramRepository {
|
||||
@override
|
||||
Future<void> replaceExercises(Program program, DateTime deletedAt) async {}
|
||||
|
||||
@override
|
||||
Future<Program?> findById(String id) async => null;
|
||||
|
||||
@override
|
||||
Future<List<Program>> listActive() async => const [];
|
||||
|
||||
@override
|
||||
Future<void> save(Program program) async {}
|
||||
|
||||
@override
|
||||
Future<void> saveExercise(ProgramExercise exercise) async {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user