Files
GameTime/test/presentation/profile_screen_test.dart
Blomios c0dbce275b 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>
2026-07-19 21:41:18 +02:00

229 lines
6.1 KiB
Dart

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/profile_screen.dart';
void main() {
testWidgets('le profil déconnecté invite sans bloquer le reste', (
tester,
) async {
final harness = _AuthHarness();
await tester.pumpWidget(
MaterialApp(home: ProfileScreen(authUseCases: harness.useCases)),
);
await tester.pump();
expect(find.text('Compte optionnel'), findsOneWidget);
expect(find.text('Créer un compte'), findsOneWidget);
expect(find.text('Se connecter'), findsOneWidget);
expect(find.byType(AlertDialog), findsNothing);
});
testWidgets('la connexion invalide affiche une erreur inline', (
tester,
) async {
final harness = _AuthHarness()
..remote.loginFailure = RemoteAuthFailure.invalidCredentials;
await tester.pumpWidget(
MaterialApp(home: ProfileScreen(authUseCases: harness.useCases)),
);
await tester.pump();
await tester.tap(find.text('Se connecter'));
await tester.pumpAndSettle();
await tester.enterText(
find.widgetWithText(TextFormField, 'Email'),
'a@b.fr',
);
await tester.enterText(
find.widgetWithText(TextFormField, 'Mot de passe'),
'password1',
);
await tester.tap(find.widgetWithText(FilledButton, 'Se connecter'));
await tester.pumpAndSettle();
expect(find.text('Email ou mot de passe incorrect.'), findsOneWidget);
expect(find.byType(AlertDialog), findsNothing);
});
testWidgets('la connexion réussie affiche le profil connecté', (
tester,
) async {
final harness = _AuthHarness();
await tester.pumpWidget(
MaterialApp(home: ProfileScreen(authUseCases: harness.useCases)),
);
await tester.pump();
await tester.tap(find.text('Se connecter'));
await tester.pumpAndSettle();
await tester.enterText(
find.widgetWithText(TextFormField, 'Email'),
'alex@example.com',
);
await tester.enterText(
find.widgetWithText(TextFormField, 'Mot de passe'),
'password1',
);
await tester.tap(find.widgetWithText(FilledButton, 'Se connecter'));
await tester.pumpAndSettle();
expect(find.text('alex@example.com'), findsOneWidget);
expect(find.text('Synchronisation'), findsOneWidget);
expect(find.text('Partages reçus'), findsOneWidget);
});
testWidgets('la déconnexion repasse au profil déconnecté', (tester) async {
final harness = _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',
);
await tester.pumpWidget(
MaterialApp(home: ProfileScreen(authUseCases: harness.useCases)),
);
await tester.pump();
expect(find.text('Alex'), findsOneWidget);
await tester.tap(find.text('Se déconnecter'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, 'Se déconnecter'));
await tester.pumpAndSettle();
expect(harness.tokenStore.token, isNull);
expect(harness.accountRepository.session?.isLoggedIn, isFalse);
expect(find.text('Compte optionnel'), findsOneWidget);
});
}
final class _AuthHarness {
_AuthHarness({UserAccountSession? initialSession, String? initialToken})
: tokenStore = _FakeAuthTokenStore(initialToken),
accountRepository = _FakeOnlineAccountRepository(initialSession),
remote = _FakeRemoteAuthApi() {
useCases = AuthUseCases(
tokenStore: tokenStore,
accountRepository: accountRepository,
remoteAuthApi: remote,
clock: const _FakeClock(),
ids: _FakeIds(),
);
}
final _FakeAuthTokenStore tokenStore;
final _FakeOnlineAccountRepository accountRepository;
final _FakeRemoteAuthApi remote;
late final AuthUseCases useCases;
}
final class _FakeClock implements Clock {
const _FakeClock();
@override
DateTime now() => DateTime.utc(2026, 7, 17, 12);
}
final class _FakeIds implements IdGenerator {
var _next = 0;
@override
String newId() {
_next += 1;
return 'local-account-$_next';
}
}
final class _FakeAuthTokenStore implements AuthTokenStore {
_FakeAuthTokenStore(this.token);
String? token;
@override
Future<void> clearToken() async {
token = null;
}
@override
Future<String?> readToken() async => token;
@override
Future<void> saveToken(String token, DateTime expiresAt) async {
this.token = token;
}
}
final class _FakeOnlineAccountRepository implements OnlineAccountRepository {
_FakeOnlineAccountRepository(this.session);
UserAccountSession? session;
@override
Future<void> clearSession() async {
session = null;
}
@override
Future<UserAccountSession?> currentSession() async => session;
@override
Future<void> saveSession(UserAccountSession session) async {
this.session = session;
}
}
final class _FakeRemoteAuthApi implements RemoteAuthApi {
RemoteAuthFailure? loginFailure;
RemoteAuthFailure? registerFailure;
@override
Future<RemoteAuthResult> login({
required String email,
required String password,
}) async {
final failure = loginFailure;
if (failure != null) {
throw RemoteAuthException(failure);
}
return RemoteAuthResult(
userId: 'server-user-1',
email: email.trim(),
token: 'token-1',
expiresAt: DateTime.utc(2026, 7, 18),
);
}
@override
Future<void> logout(String token) async {}
@override
Future<RemoteAuthResult> register({
required String email,
required String password,
String? displayName,
}) async {
final failure = registerFailure;
if (failure != null) {
throw RemoteAuthException(failure);
}
return RemoteAuthResult(
userId: 'server-user-1',
email: email.trim(),
token: 'token-1',
expiresAt: DateTime.utc(2026, 7, 18),
);
}
}