feat(online): sync client incrémentale LWW et profil/écrans auth (tickets #65, #67)

Ajoute la synchronisation client incrémentale LWW vers l'API serveur
(application/use_cases.dart: SyncUseCases, ports.dart, migration
Drift schemaVersion 10→11 pour les métadonnées de sync et mappings de
ressources, infrastructure/remote/sync_api.dart) et l'écran de profil
avec entrée sur l'accueil (presentation/profile_screen.dart,
home_screen.dart, presentation.dart). Développés dans le même
worktree partagé par DevBackend et DevFrontend ; commit combiné car
test/presentation/home_screen_test.dart mélange authentiquement les
deux tickets (le test de l'entrée Profil et la mise à jour du fake de
bootstrap requise par le nouveau getter syncUseCases sur
AppDependencies). Corrige au passage une couleur `crimson` inexistante
dans le thème (remplacée par colorScheme.error) et une signature de
paramètres positionnels/nommés incohérente sur un fake de test.
flutter pub get OK, build_runner OK, dart format appliqué, analyze
propre (mêmes infos préexistantes), 119/119 tests verts, build APK
debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 21:41:18 +02:00
parent b643988986
commit c0dbce275b
16 changed files with 3913 additions and 3 deletions

View File

@ -9,6 +9,21 @@ import 'package:gametime/domain/domain.dart';
import 'package:gametime/presentation/home_screen.dart';
void main() {
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 {
@ -122,6 +137,15 @@ final class _FakeBootstrap implements AppDependencies {
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',
),
activeWorkoutSessionUseCases = ActiveWorkoutSessionUseCases(
sessionRepository: activeRepository,
templateRepository: _FakeWorkoutTemplateRepository(),
@ -180,6 +204,9 @@ final class _FakeBootstrap implements AppDependencies {
@override
final AuthUseCases authUseCases;
@override
final SyncUseCases syncUseCases;
@override
final ExerciseUseCases exerciseUseCases;
@ -303,6 +330,66 @@ final class _FakeRemoteAuthApi implements RemoteAuthApi {
}
}
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 _FakeActiveSessionRepository implements ActiveSessionRepository {
ActiveWorkoutSession? session;
final restStates = <ActiveRestState>[];