Files
GameTime/test/presentation/home_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

821 lines
22 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:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gametime/application/application.dart';
import 'package:gametime/application/app_bootstrap.dart';
import 'package:gametime/domain/domain.dart';
import 'package:gametime/presentation/home_screen.dart';
void main() {
testWidgets('lentré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('lentrée Profil est présente sur laccueil', (tester) async {
await tester.pumpWidget(
MaterialApp(
navigatorObservers: [homeRouteObserver],
home: HomeScreen(
bootstrap: _FakeBootstrap(_FakeActiveSessionRepository()),
),
),
);
await tester.pump();
expect(find.text('Profil'), findsOneWidget);
expect(find.text('Compte, synchronisation et partages'), findsOneWidget);
});
testWidgets('le bandeau de reprise apparaît au retour sur laccueil', (
tester,
) async {
final navigatorKey = GlobalKey<NavigatorState>();
final activeRepository = _FakeActiveSessionRepository();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigatorKey,
navigatorObservers: [homeRouteObserver],
home: HomeScreen(bootstrap: _FakeBootstrap(activeRepository)),
),
);
await tester.pump();
await tester.pump();
expect(find.textContaining('Séance en cours'), findsNothing);
unawaited(
navigatorKey.currentState!.push<void>(
MaterialPageRoute(
builder: (context) => const Scaffold(body: Text('Séance active')),
),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 350));
activeRepository.session = ActiveWorkoutSession(
metadata: _metadata('session-1'),
sourceWorkoutTemplateId: 'template-1',
status: ActiveWorkoutStatus.paused,
startedAt: DateTime.utc(2026, 7, 17, 12),
pausedAt: DateTime.utc(2026, 7, 17, 12, 5),
lastPersistedAt: DateTime.utc(2026, 7, 17, 12, 5),
elapsedActiveMs: 300000,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 1,
resolvedTemplateSnapshotJson: _sessionSnapshot(),
);
navigatorKey.currentState!.pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 350));
await tester.pump();
expect(
find.text('Séance en cours — Reprendre · Squat, série 2/3'),
findsOneWidget,
);
});
testWidgets('le bandeau de reprise disparaît au retour après abandon', (
tester,
) async {
final navigatorKey = GlobalKey<NavigatorState>();
final activeRepository = _FakeActiveSessionRepository()
..session = ActiveWorkoutSession(
metadata: _metadata('session-1'),
sourceWorkoutTemplateId: 'template-1',
status: ActiveWorkoutStatus.paused,
startedAt: DateTime.utc(2026, 7, 17, 12),
pausedAt: DateTime.utc(2026, 7, 17, 12, 5),
lastPersistedAt: DateTime.utc(2026, 7, 17, 12, 5),
elapsedActiveMs: 300000,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 1,
resolvedTemplateSnapshotJson: _sessionSnapshot(),
);
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigatorKey,
navigatorObservers: [homeRouteObserver],
home: HomeScreen(bootstrap: _FakeBootstrap(activeRepository)),
),
);
await tester.pump();
await tester.pump();
expect(find.textContaining('Séance en cours'), findsOneWidget);
unawaited(
navigatorKey.currentState!.push<void>(
MaterialPageRoute(
builder: (context) => const Scaffold(body: Text('Séance active')),
),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 350));
activeRepository.session = null;
navigatorKey.currentState!.pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 350));
await tester.pump();
expect(find.textContaining('Séance en cours'), findsNothing);
});
}
final class _FakeBootstrap implements AppDependencies {
_FakeBootstrap(_FakeActiveSessionRepository activeRepository)
: authUseCases = AuthUseCases(
tokenStore: _FakeAuthTokenStore(),
accountRepository: _FakeOnlineAccountRepository(),
remoteAuthApi: _FakeRemoteAuthApi(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
),
syncUseCases = SyncUseCases(
tokenStore: _FakeAuthTokenStore(),
remoteSyncApi: _FakeRemoteSyncApi(),
metadataRepository: _FakeSyncMetadataRepository(),
mappingRepository: _FakeRemoteResourceMappingRepository(),
localChanges: _FakeLocalSyncChangeRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
deviceId: 'device-1',
),
shareUseCases = ShareUseCases(
tokenStore: _FakeAuthTokenStore(),
remoteShareApi: _FakeRemoteShareApi(),
inboxRepository: _FakeShareInboxRepository(),
pendingActionRepository: _FakePendingShareActionRepository(),
localChanges: _FakeLocalSyncChangeRepository(),
programRepository: _FakeProgramRepository(),
templateRepository: _FakeWorkoutTemplateRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
),
activeWorkoutSessionUseCases = ActiveWorkoutSessionUseCases(
sessionRepository: activeRepository,
templateRepository: _FakeWorkoutTemplateRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
),
activeExerciseStepUseCases = ActiveExerciseStepUseCases(
sessionRepository: activeRepository,
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
),
closeWorkoutSessionUseCase = CloseWorkoutSessionUseCase(
sessionRepository: activeRepository,
historyRepository: _FakeWorkoutHistoryRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
),
exerciseUseCases = ExerciseUseCases(
repository: _FakeExerciseRepository(),
programRepository: _FakeProgramRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
),
mediaUseCases = MediaUseCases(
mediaRepository: _FakeMediaAssetRepository(),
exerciseRepository: _FakeExerciseRepository(),
storage: _FakeMediaStorage(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
),
programUseCases = ProgramUseCases(
programRepository: _FakeProgramRepository(),
exerciseRepository: _FakeExerciseRepository(),
templateRepository: _FakeWorkoutTemplateRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
),
workoutTemplateUseCases = WorkoutTemplateUseCases(
templateRepository: _FakeWorkoutTemplateRepository(),
programRepository: _FakeProgramRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
),
workoutHistoryUseCases = WorkoutHistoryUseCases(
repository: _FakeWorkoutHistoryRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
),
progressionStatsUseCase = ProgressionStatsUseCase(
repository: _FakeProgressionStatsRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
),
exercisePerformanceReferenceUseCase = ExercisePerformanceReferenceUseCase(
repository: _FakeExercisePerformanceReferenceRepository(),
);
@override
final AuthUseCases authUseCases;
@override
final SyncUseCases syncUseCases;
@override
final ShareUseCases shareUseCases;
@override
final ExerciseUseCases exerciseUseCases;
@override
final MediaUseCases mediaUseCases;
@override
final ProgramUseCases programUseCases;
@override
final WorkoutTemplateUseCases workoutTemplateUseCases;
@override
final ActiveWorkoutSessionUseCases activeWorkoutSessionUseCases;
@override
final ActiveExerciseStepUseCases activeExerciseStepUseCases;
@override
final CloseWorkoutSessionUseCase closeWorkoutSessionUseCase;
@override
final WorkoutHistoryUseCases workoutHistoryUseCases;
@override
final ProgressionStatsUseCase progressionStatsUseCase;
@override
final ExercisePerformanceReferenceUseCase exercisePerformanceReferenceUseCase;
}
String _sessionSnapshot() {
return jsonEncode({
'name': 'Séance jambes',
'programs': [
{
'id': 'template-program-1',
'programNameSnapshot': 'Programme jambes',
'programSnapshotJson': jsonEncode({
'exercises': [
{
'id': 'exercise-1',
'exerciseNameSnapshot': 'Squat',
'setsCount': 3,
'timeEnabled': true,
'repsEnabled': false,
'scoreEnabled': false,
'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',
);
}
final class _FakeClock implements Clock {
const _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 _FakeAuthTokenStore implements AuthTokenStore {
@override
Future<void> clearToken() async {}
@override
Future<String?> readToken() async => null;
@override
Future<void> saveToken(String token, DateTime expiresAt) async {}
}
final class _FakeOnlineAccountRepository implements OnlineAccountRepository {
@override
Future<void> clearSession() async {}
@override
Future<UserAccountSession?> currentSession() async => null;
@override
Future<void> saveSession(UserAccountSession session) async {}
}
final class _FakeRemoteAuthApi implements RemoteAuthApi {
@override
Future<RemoteAuthResult> login({
required String email,
required String password,
}) async {
throw const RemoteAuthException(RemoteAuthFailure.network);
}
@override
Future<void> logout(String token) async {}
@override
Future<RemoteAuthResult> register({
required String email,
required String password,
String? displayName,
}) async {
throw const RemoteAuthException(RemoteAuthFailure.network);
}
}
final class _FakeRemoteSyncApi implements RemoteSyncApi {
@override
Future<RemoteSyncPullResult> pull({
required String? since,
required String token,
}) async {
return const RemoteSyncPullResult(serverCursor: null, items: []);
}
@override
Future<RemoteSyncPushResult> push({
required String deviceId,
required List<RemoteSyncPushItem> items,
required String token,
}) async {
return const RemoteSyncPushResult(serverCursor: null, results: []);
}
}
final class _FakeSyncMetadataRepository implements SyncMetadataRepository {
SyncMetadataSnapshot _metadata = const SyncMetadataSnapshot();
@override
Future<SyncMetadataSnapshot> read() async => _metadata;
@override
Future<void> save(SyncMetadataSnapshot metadata) async {
_metadata = metadata;
}
}
final class _FakeRemoteResourceMappingRepository
implements RemoteResourceMappingRepository {
@override
Future<RemoteResourceMapping?> find({
required SyncResourceType resourceType,
required String clientId,
}) async {
return null;
}
@override
Future<void> save(RemoteResourceMapping mapping) async {}
}
final class _FakeLocalSyncChangeRepository
implements LocalSyncChangeRepository {
@override
Future<bool> applyRemoteItem(RemoteSyncedItem item) async => false;
@override
Future<List<PendingSyncChange>> listPendingChanges() async => const [];
@override
Future<void> markChangesSynced(
List<String> changeLogIds,
DateTime syncedAt,
) async {}
}
final class _FakeRemoteShareApi implements RemoteShareApi {
@override
Future<RemoteSyncedItem> acceptShare(String shareId, String token) async {
throw const RemoteAuthException(RemoteAuthFailure.network);
}
@override
Future<void> declineShare(String shareId, String token) async {}
@override
Future<List<ShareInboxItem>> fetchInbox(String token) async => const [];
@override
Future<void> revokeShare(String shareId, String token) async {}
@override
Future<RemoteShareSendResult> sendShare({
required ShareResourceType resourceType,
required Map<String, Object?> payload,
required List<String> recipientEmails,
required String token,
}) async {
return const RemoteShareSendResult(
shareId: 'share-1',
recipientUserIds: [],
unresolvedEmails: [],
);
}
}
final class _FakeShareInboxRepository implements ShareInboxRepository {
@override
Future<ShareInboxItem?> findByShareId(String shareId) async => null;
@override
Future<List<ShareInboxItem>> listAll() async => const [];
@override
Future<void> markStatus(
String shareId,
ShareInboxStatus status,
DateTime respondedAt,
) async {}
@override
Future<void> upsert(ShareInboxItem item) async {}
@override
Future<void> upsertAll(List<ShareInboxItem> items) async {}
}
final class _FakePendingShareActionRepository
implements PendingShareActionRepository {
@override
Future<void> add(PendingShareAction action) async {}
@override
Future<List<PendingShareAction>> listPending() async => const [];
@override
Future<void> markFailed(String id, DateTime attemptedAt) async {}
@override
Future<void> markSucceeded(String id, DateTime attemptedAt) async {}
}
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
ActiveWorkoutSession? session;
final restStates = <ActiveRestState>[];
final setTimerStates = <ActiveSetTimerState>[];
final scoreStopwatchStates = <ActiveScoreStopwatchState>[];
final results = <ActiveSetResult>[];
@override
Future<ActiveWorkoutSession?> findById(String id) async {
return session?.metadata.id == id ? session : null;
}
@override
Future<ActiveWorkoutSession?> findOpen() async => session;
@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 {
return null;
}
@override
Future<ActiveSetTimerState?> findSetTimerState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
}) async {
return null;
}
@override
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
}) async {
return null;
}
@override
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
return restStates;
}
@override
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
String sessionId,
) async {
return scoreStopwatchStates;
}
@override
Future<List<ActiveSetTimerState>> listSetTimerStates(String sessionId) async {
return setTimerStates;
}
@override
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
return results;
}
@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 {
this.session = session;
}
@override
Future<void> saveRestState(ActiveRestState restState) async {
restStates.add(restState);
}
@override
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {
scoreStopwatchStates.add(state);
}
@override
Future<void> saveSetTimerState(ActiveSetTimerState state) async {
setTimerStates.add(state);
}
@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 {
results.add(result);
}
}
final class _FakeExerciseRepository implements ExerciseRepository {
@override
Future<Exercise?> findById(String id) async => null;
@override
Future<bool> isReferencedByProgram(String id) async => false;
@override
Future<List<Exercise>> listActive() async => const [];
@override
Future<void> save(Exercise exercise) async {}
}
final class _FakeMediaAssetRepository implements MediaAssetRepository {
@override
Future<MediaAsset?> findById(String id) async => null;
@override
Future<List<MediaAsset>> listActive() async => const [];
@override
Future<void> save(MediaAsset mediaAsset) async {}
}
final class _FakeMediaStorage implements LocalMediaStorage {
@override
Future<void> deleteByLocalUri(String localUri) async {}
@override
Future<StoredMediaFile> importFile({
required String sourcePath,
required MediaKind kind,
required String stableFileName,
}) async {
return StoredMediaFile(
localUri: 'file:///$stableFileName',
mimeType: 'image/png',
sizeBytes: 0,
);
}
@override
Future<Set<String>> listManagedLocalUris() async => const {};
}
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 _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 _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 {}
@override
Future<void> saveStepResult(WorkoutHistoryStepResult result) async {}
}
final class _FakeProgressionStatsRepository
implements ProgressionStatsRepository {
@override
Future<ProgressionGlobalStatsData> readGlobalStats(
ProgressionDateRange range,
) async {
return const ProgressionGlobalStatsData(
completedSessionCount: 0,
totalActiveMs: 0,
activeWeekStarts: [],
hasAnyCompletedHistory: false,
);
}
@override
Future<List<ProgressionExerciseOptionData>> listExerciseOptions(
ProgressionDateRange range,
) async {
return const [];
}
@override
Future<List<ProgressionMeasureOptionData>> listMeasureOptions({
required ProgressionDateRange range,
required String exerciseKey,
}) async {
return const [];
}
@override
Future<ProgressionExerciseSeriesData> readExerciseSeries({
required ProgressionDateRange range,
required String exerciseKey,
required ProgressionMeasure measure,
}) async {
return ProgressionExerciseSeriesData(
exerciseKey: exerciseKey,
exerciseNameSnapshot: exerciseKey,
measure: ProgressionMeasureOptionData(
measure: measure,
label: 'Score',
lowerIsBetter: false,
),
points: const [],
hasAnyAllTimeData: false,
hasAnyCompletedExerciseResult: false,
);
}
}
final class _FakeExercisePerformanceReferenceRepository
implements ExercisePerformanceReferenceRepository {
@override
Future<bool> hasAnyCompletedHistoryForExercise(String exerciseId) async {
return false;
}
@override
Future<WorkoutHistorySetPerformance?> findLatestSetPerformance({
required String exerciseId,
required ActivePerformanceMeasures activeMeasures,
required int currentSetIndex,
}) async {
return null;
}
@override
Future<WorkoutHistoryMetricPerformance?> findBestMetricPerformance({
required String exerciseId,
required PerformanceMetric metric,
required ScoreInputMode scoreInputMode,
}) async {
return null;
}
}