feat(online): statut de synchronisation discret et action manuelle (ticket #68)
Affiche le statut de synchronisation sur profile_screen.dart et home_screen.dart avec une action de déclenchement manuel, en formulation neutre (jamais "Erreur"/"Échec" ni couleur alarmante : "Synchronisation en attente", "Nouvelle tentative automatique"). flutter pub get OK, dart format appliqué, analyze propre (mêmes infos préexistantes), 127/127 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -11,7 +11,12 @@ void main() {
|
||||
final harness = _AuthHarness();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: ProfileScreen(authUseCases: harness.useCases)),
|
||||
MaterialApp(
|
||||
home: ProfileScreen(
|
||||
authUseCases: harness.useCases,
|
||||
syncUseCases: harness.syncUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
@ -28,7 +33,12 @@ void main() {
|
||||
..remote.loginFailure = RemoteAuthFailure.invalidCredentials;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: ProfileScreen(authUseCases: harness.useCases)),
|
||||
MaterialApp(
|
||||
home: ProfileScreen(
|
||||
authUseCases: harness.useCases,
|
||||
syncUseCases: harness.syncUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
@ -55,7 +65,12 @@ void main() {
|
||||
final harness = _AuthHarness();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: ProfileScreen(authUseCases: harness.useCases)),
|
||||
MaterialApp(
|
||||
home: ProfileScreen(
|
||||
authUseCases: harness.useCases,
|
||||
syncUseCases: harness.syncUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
@ -92,7 +107,12 @@ void main() {
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: ProfileScreen(authUseCases: harness.useCases)),
|
||||
MaterialApp(
|
||||
home: ProfileScreen(
|
||||
authUseCases: harness.useCases,
|
||||
syncUseCases: harness.syncUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
@ -107,13 +127,94 @@ void main() {
|
||||
expect(harness.accountRepository.session?.isLoggedIn, isFalse);
|
||||
expect(find.text('Compte optionnel'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'le bouton de synchronisation lance une synchronisation manuelle',
|
||||
(tester) async {
|
||||
final harness = _AuthHarness.connected();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ProfileScreen(
|
||||
authUseCases: harness.useCases,
|
||||
syncUseCases: harness.syncUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Synchroniser maintenant'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Synchroniser maintenant'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(harness.syncRemote.pullCalls, 1);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('un souci de synchronisation reste neutre dans le profil', (
|
||||
tester,
|
||||
) async {
|
||||
final harness = _AuthHarness.connected()..syncRemote.throwOnPull = true;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ProfileScreen(
|
||||
authUseCases: harness.useCases,
|
||||
syncUseCases: harness.syncUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('Synchroniser maintenant'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(harness.syncMetadata.metadata.status, OnlineSyncStatus.failure);
|
||||
expect(find.text('Synchronisation en attente'), findsOneWidget);
|
||||
expect(find.textContaining('Erreur'), findsNothing);
|
||||
expect(find.byType(AlertDialog), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('le statut se met à jour après une synchronisation réussie', (
|
||||
tester,
|
||||
) async {
|
||||
final harness = _AuthHarness.connected();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ProfileScreen(
|
||||
authUseCases: harness.useCases,
|
||||
syncUseCases: harness.syncUseCases,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Synchronisation en attente'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Synchroniser maintenant'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.textContaining('Dernière synchro :'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
final class _AuthHarness {
|
||||
_AuthHarness({UserAccountSession? initialSession, String? initialToken})
|
||||
: tokenStore = _FakeAuthTokenStore(initialToken),
|
||||
accountRepository = _FakeOnlineAccountRepository(initialSession),
|
||||
remote = _FakeRemoteAuthApi() {
|
||||
remote = _FakeRemoteAuthApi(),
|
||||
syncRemote = _FakeRemoteSyncApi(),
|
||||
syncMetadata = _FakeSyncMetadataRepository(),
|
||||
syncMappings = _FakeRemoteResourceMappingRepository(),
|
||||
syncChanges = _FakeLocalSyncChangeRepository() {
|
||||
useCases = AuthUseCases(
|
||||
tokenStore: tokenStore,
|
||||
accountRepository: accountRepository,
|
||||
@ -121,12 +222,41 @@ final class _AuthHarness {
|
||||
clock: const _FakeClock(),
|
||||
ids: _FakeIds(),
|
||||
);
|
||||
syncUseCases = SyncUseCases(
|
||||
tokenStore: tokenStore,
|
||||
remoteSyncApi: syncRemote,
|
||||
metadataRepository: syncMetadata,
|
||||
mappingRepository: syncMappings,
|
||||
localChanges: syncChanges,
|
||||
clock: const _FakeClock(),
|
||||
deviceId: 'device-1',
|
||||
);
|
||||
}
|
||||
|
||||
factory _AuthHarness.connected() {
|
||||
return _AuthHarness(
|
||||
initialSession: UserAccountSession(
|
||||
id: 'account-1',
|
||||
serverUserId: 'server-user-1',
|
||||
email: 'alex@example.com',
|
||||
displayName: 'Alex',
|
||||
isLoggedIn: true,
|
||||
createdAt: DateTime.utc(2026, 7, 17),
|
||||
updatedAt: DateTime.utc(2026, 7, 17),
|
||||
),
|
||||
initialToken: 'token-1',
|
||||
);
|
||||
}
|
||||
|
||||
final _FakeAuthTokenStore tokenStore;
|
||||
final _FakeOnlineAccountRepository accountRepository;
|
||||
final _FakeRemoteAuthApi remote;
|
||||
final _FakeRemoteSyncApi syncRemote;
|
||||
final _FakeSyncMetadataRepository syncMetadata;
|
||||
final _FakeRemoteResourceMappingRepository syncMappings;
|
||||
final _FakeLocalSyncChangeRepository syncChanges;
|
||||
late final AuthUseCases useCases;
|
||||
late final SyncUseCases syncUseCases;
|
||||
}
|
||||
|
||||
final class _FakeClock implements Clock {
|
||||
@ -226,3 +356,76 @@ final class _FakeRemoteAuthApi implements RemoteAuthApi {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class _FakeRemoteSyncApi implements RemoteSyncApi {
|
||||
var pullCalls = 0;
|
||||
var pushCalls = 0;
|
||||
var throwOnPull = false;
|
||||
|
||||
@override
|
||||
Future<RemoteSyncPushResult> push({
|
||||
required String deviceId,
|
||||
required List<RemoteSyncPushItem> items,
|
||||
required String token,
|
||||
}) async {
|
||||
pushCalls += 1;
|
||||
return const RemoteSyncPushResult(serverCursor: null, results: []);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RemoteSyncPullResult> pull({
|
||||
required String? since,
|
||||
required String token,
|
||||
}) async {
|
||||
pullCalls += 1;
|
||||
if (throwOnPull) {
|
||||
throw Exception('temporary unavailable');
|
||||
}
|
||||
return const RemoteSyncPullResult(serverCursor: 'cursor-1', items: []);
|
||||
}
|
||||
}
|
||||
|
||||
final class _FakeSyncMetadataRepository implements SyncMetadataRepository {
|
||||
var metadata = const SyncMetadataSnapshot();
|
||||
|
||||
@override
|
||||
Future<SyncMetadataSnapshot> read() async => metadata;
|
||||
|
||||
@override
|
||||
Future<void> save(SyncMetadataSnapshot metadata) async {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
final class _FakeRemoteResourceMappingRepository
|
||||
implements RemoteResourceMappingRepository {
|
||||
final _mappings = <String, RemoteResourceMapping>{};
|
||||
|
||||
@override
|
||||
Future<RemoteResourceMapping?> find({
|
||||
required SyncResourceType resourceType,
|
||||
required String clientId,
|
||||
}) async {
|
||||
return _mappings['${resourceType.name}:$clientId'];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(RemoteResourceMapping mapping) async {
|
||||
_mappings['${mapping.resourceType.name}:${mapping.clientId}'] = mapping;
|
||||
}
|
||||
}
|
||||
|
||||
final class _FakeLocalSyncChangeRepository
|
||||
implements LocalSyncChangeRepository {
|
||||
@override
|
||||
Future<bool> applyRemoteItem(RemoteSyncedItem item) async => true;
|
||||
|
||||
@override
|
||||
Future<List<PendingSyncChange>> listPendingChanges() async => const [];
|
||||
|
||||
@override
|
||||
Future<void> markChangesSynced(
|
||||
List<String> changeLogIds,
|
||||
DateTime syncedAt,
|
||||
) async {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user