fix(chat): Correction race condition + Implémentation TODOs

## Corrections Critiques

### Race Condition - Statuts de Messages
- Fix : Les icônes de statut (✓, ✓✓, ✓✓ bleu) ne s'affichaient pas
- Cause : WebSocket delivery confirmations arrivaient avant messages locaux
- Solution : Pattern Optimistic UI dans chat_bloc.dart
  - Création message temporaire immédiate
  - Ajout à la liste AVANT requête HTTP
  - Remplacement par message serveur à la réponse
- Fichier : lib/presentation/state_management/chat_bloc.dart

## Implémentation TODOs (13/21)

### Social (social_header_widget.dart)
-  Copier lien du post dans presse-papiers
-  Partage natif via Share.share()
-  Dialogue de signalement avec 5 raisons

### Partage (share_post_dialog.dart)
-  Interface sélection d'amis avec checkboxes
-  Partage externe via Share API

### Média (media_upload_service.dart)
-  Parsing JSON réponse backend
-  Méthode deleteMedia() pour suppression
-  Génération miniature vidéo

### Posts (create_post_dialog.dart, edit_post_dialog.dart)
-  Extraction URL depuis uploads
-  Documentation chargement médias

### Chat (conversations_screen.dart)
-  Navigation vers notifications
-  ConversationSearchDelegate pour recherche

## Nouveaux Fichiers

### Configuration
- build-prod.ps1 : Script build production avec dart-define
- lib/core/constants/env_config.dart : Gestion environnements

### Documentation
- TODOS_IMPLEMENTED.md : Documentation complète TODOs

## Améliorations

### Architecture
- Refactoring injection de dépendances
- Amélioration routing et navigation
- Optimisation providers (UserProvider, FriendsProvider)

### UI/UX
- Amélioration thème et couleurs
- Optimisation animations
- Meilleure gestion erreurs

### Services
- Configuration API avec env_config
- Amélioration datasources (events, users)
- Optimisation modèles de données
This commit is contained in:
dahoud
2026-01-10 10:43:17 +00:00
parent 06031b01f2
commit 92612abbd7
321 changed files with 43137 additions and 4285 deletions

View File

@@ -0,0 +1,253 @@
import 'package:afterwork/domain/entities/event.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Event Entity', () {
final tStartDate = DateTime(2026, 1, 15, 19, 0);
test('should create an Event with required fields', () {
// Arrange & Act
final event = Event(
id: '1',
title: 'After-work Tech',
description: 'Soirée networking',
startDate: tStartDate,
location: 'Paris, France',
category: 'Networking',
creatorEmail: 'john@example.com',
creatorFirstName: 'John',
creatorLastName: 'Doe',
creatorProfileImageUrl: 'https://example.com/profile.jpg',
);
// Assert
expect(event.id, '1');
expect(event.title, 'After-work Tech');
expect(event.description, 'Soirée networking');
expect(event.startDate, tStartDate);
expect(event.location, 'Paris, France');
expect(event.category, 'Networking');
expect(event.creatorEmail, 'john@example.com');
expect(event.creatorFirstName, 'John');
expect(event.creatorLastName, 'Doe');
expect(event.status, EventStatus.open); // Default
expect(event.participantIds, isEmpty);
expect(event.reactionsCount, 0);
expect(event.commentsCount, 0);
expect(event.sharesCount, 0);
});
test('should create an Event with optional fields', () {
// Arrange & Act
final event = Event(
id: '1',
title: 'After-work Tech',
description: 'Soirée networking',
startDate: tStartDate,
location: 'Paris, France',
category: 'Networking',
link: 'https://event.com',
imageUrl: 'https://event.com/image.jpg',
creatorEmail: 'john@example.com',
creatorFirstName: 'John',
creatorLastName: 'Doe',
creatorProfileImageUrl: 'https://example.com/profile.jpg',
participantIds: const ['user1', 'user2'],
status: EventStatus.closed,
reactionsCount: 10,
commentsCount: 5,
sharesCount: 3,
);
// Assert
expect(event.link, 'https://event.com');
expect(event.imageUrl, 'https://event.com/image.jpg');
expect(event.participantIds, ['user1', 'user2']);
expect(event.status, EventStatus.closed);
expect(event.reactionsCount, 10);
expect(event.commentsCount, 5);
expect(event.sharesCount, 3);
});
test('should return correct creator full name', () {
// Arrange & Act
final event = Event(
id: '1',
title: 'Test Event',
description: 'Test',
startDate: tStartDate,
location: 'Test Location',
category: 'Test',
creatorEmail: 'john@example.com',
creatorFirstName: 'John',
creatorLastName: 'Doe',
creatorProfileImageUrl: 'test.jpg',
);
// Assert
expect(event.creatorFullName, 'John Doe');
});
test('should return correct participants count', () {
// Arrange & Act
final event = Event(
id: '1',
title: 'Test Event',
description: 'Test',
startDate: tStartDate,
location: 'Test Location',
category: 'Test',
creatorEmail: 'test@example.com',
creatorFirstName: 'Test',
creatorLastName: 'User',
creatorProfileImageUrl: 'test.jpg',
participantIds: const ['user1', 'user2', 'user3'],
);
// Assert
expect(event.participantsCount, 3);
});
test('isOpen should return true when status is open', () {
// Arrange & Act
final event = Event(
id: '1',
title: 'Test Event',
description: 'Test',
startDate: tStartDate,
location: 'Test Location',
category: 'Test',
creatorEmail: 'test@example.com',
creatorFirstName: 'Test',
creatorLastName: 'User',
creatorProfileImageUrl: 'test.jpg',
status: EventStatus.open,
);
// Assert
expect(event.isOpen, isTrue);
expect(event.isClosed, isFalse);
expect(event.isCancelled, isFalse);
});
test('isClosed should return true when status is closed', () {
// Arrange & Act
final event = Event(
id: '1',
title: 'Test Event',
description: 'Test',
startDate: tStartDate,
location: 'Test Location',
category: 'Test',
creatorEmail: 'test@example.com',
creatorFirstName: 'Test',
creatorLastName: 'User',
creatorProfileImageUrl: 'test.jpg',
status: EventStatus.closed,
);
// Assert
expect(event.isClosed, isTrue);
expect(event.isOpen, isFalse);
expect(event.isCancelled, isFalse);
});
test('copyWith should create a new instance with updated fields', () {
// Arrange
final original = Event(
id: '1',
title: 'Original Title',
description: 'Original Description',
startDate: tStartDate,
location: 'Original Location',
category: 'Original Category',
creatorEmail: 'test@example.com',
creatorFirstName: 'Test',
creatorLastName: 'User',
creatorProfileImageUrl: 'test.jpg',
);
// Act
final updated = original.copyWith(
title: 'Updated Title',
status: EventStatus.closed,
);
// Assert
expect(updated.id, original.id); // Unchanged
expect(updated.title, 'Updated Title'); // Changed
expect(updated.description, original.description); // Unchanged
expect(updated.status, EventStatus.closed); // Changed
});
test('should support value equality using Equatable', () {
// Arrange
final event1 = Event(
id: '1',
title: 'Event',
description: 'Description',
startDate: tStartDate,
location: 'Location',
category: 'Category',
creatorEmail: 'test@example.com',
creatorFirstName: 'Test',
creatorLastName: 'User',
creatorProfileImageUrl: 'test.jpg',
);
final event2 = Event(
id: '1',
title: 'Event',
description: 'Description',
startDate: tStartDate,
location: 'Location',
category: 'Category',
creatorEmail: 'test@example.com',
creatorFirstName: 'Test',
creatorLastName: 'User',
creatorProfileImageUrl: 'test.jpg',
);
// Assert
expect(event1, equals(event2));
});
});
group('EventStatus', () {
test('fromString should convert "ouvert" to open', () {
expect(EventStatus.fromString('ouvert'), EventStatus.open);
expect(EventStatus.fromString('open'), EventStatus.open);
});
test('fromString should convert "fermé" to closed', () {
expect(EventStatus.fromString('fermé'), EventStatus.closed);
expect(EventStatus.fromString('ferme'), EventStatus.closed);
expect(EventStatus.fromString('closed'), EventStatus.closed);
});
test('fromString should convert "annulé" to cancelled', () {
expect(EventStatus.fromString('annulé'), EventStatus.cancelled);
expect(EventStatus.fromString('annule'), EventStatus.cancelled);
expect(EventStatus.fromString('cancelled'), EventStatus.cancelled);
});
test('fromString should convert "terminé" to completed', () {
expect(EventStatus.fromString('terminé'), EventStatus.completed);
expect(EventStatus.fromString('termine'), EventStatus.completed);
expect(EventStatus.fromString('completed'), EventStatus.completed);
});
test('fromString should return open for unknown status', () {
expect(EventStatus.fromString('unknown'), EventStatus.open);
expect(EventStatus.fromString(''), EventStatus.open);
});
test('toApiString should convert status to French API strings', () {
expect(EventStatus.open.toApiString(), 'ouvert');
expect(EventStatus.closed.toApiString(), 'fermé');
expect(EventStatus.cancelled.toApiString(), 'annulé');
expect(EventStatus.completed.toApiString(), 'terminé');
});
});
}

View File

@@ -0,0 +1,210 @@
import 'package:afterwork/domain/entities/friend.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Friend Entity', () {
const tFriendId = 'friend123';
const tFirstName = 'Jane';
const tLastName = 'Smith';
const tEmail = 'jane.smith@example.com';
const tImageUrl = 'https://example.com/jane.jpg';
test('should create a Friend with required fields', () {
// Act
final friend = Friend(
friendId: tFriendId,
friendFirstName: tFirstName,
friendLastName: tLastName,
);
// Assert
expect(friend.friendId, tFriendId);
expect(friend.friendFirstName, tFirstName);
expect(friend.friendLastName, tLastName);
expect(friend.status, FriendStatus.unknown); // Default
expect(friend.isOnline, isFalse); // Default
expect(friend.isBestFriend, isFalse); // Default
expect(friend.hasKnownSinceChildhood, isFalse); // Default
});
test('should create a Friend with optional fields', () {
// Act
final friend = Friend(
friendId: tFriendId,
friendFirstName: tFirstName,
friendLastName: tLastName,
email: tEmail,
imageUrl: tImageUrl,
status: FriendStatus.accepted,
isOnline: true,
isBestFriend: true,
hasKnownSinceChildhood: true,
);
// Assert
expect(friend.email, tEmail);
expect(friend.imageUrl, tImageUrl);
expect(friend.status, FriendStatus.accepted);
expect(friend.isOnline, isTrue);
expect(friend.isBestFriend, isTrue);
expect(friend.hasKnownSinceChildhood, isTrue);
});
test('should use default name when firstName is not provided', () {
// Act
final friend = Friend(
friendId: tFriendId,
);
// Assert
expect(friend.friendFirstName, 'Ami inconnu');
expect(friend.friendLastName, '');
});
test('should support value equality using Equatable', () {
// Arrange
final friend1 = Friend(
friendId: tFriendId,
friendFirstName: tFirstName,
friendLastName: tLastName,
);
final friend2 = Friend(
friendId: tFriendId,
friendFirstName: tFirstName,
friendLastName: tLastName,
);
// Assert
expect(friend1, equals(friend2));
expect(friend1.hashCode, equals(friend2.hashCode));
});
test('fromJson should create Friend from valid JSON', () {
// Arrange
final json = {
'friendId': tFriendId,
'friendFirstName': tFirstName,
'friendLastName': tLastName,
'email': tEmail,
'friendProfileImageUrl': tImageUrl,
'status': 'accepted',
'isOnline': true,
'isBestFriend': false,
'hasKnownSinceChildhood': false,
};
// Act
final friend = Friend.fromJson(json);
// Assert
expect(friend.friendId, tFriendId);
expect(friend.friendFirstName, tFirstName);
expect(friend.friendLastName, tLastName);
expect(friend.email, tEmail);
expect(friend.imageUrl, tImageUrl);
expect(friend.status, FriendStatus.accepted);
expect(friend.isOnline, isTrue);
});
test('fromJson should use default values for missing fields', () {
// Arrange
final json = {
'friendId': tFriendId,
};
// Act
final friend = Friend.fromJson(json);
// Assert
expect(friend.friendId, tFriendId);
expect(friend.friendFirstName, 'Ami inconnu');
expect(friend.friendLastName, '');
expect(friend.status, FriendStatus.unknown);
});
test('fromJson should throw when friendId is missing', () {
// Arrange
final json = <String, dynamic>{};
// Act & Assert
expect(
() => Friend.fromJson(json),
throwsA(isA<ArgumentError>()),
);
});
test('toJson should serialize Friend correctly', () {
// Arrange
final friend = Friend(
friendId: tFriendId,
friendFirstName: tFirstName,
friendLastName: tLastName,
email: tEmail,
imageUrl: tImageUrl,
status: FriendStatus.accepted,
isOnline: true,
isBestFriend: false,
hasKnownSinceChildhood: false,
);
// Act
final json = friend.toJson();
// Assert
expect(json['friendId'], tFriendId);
expect(json['friendFirstName'], tFirstName);
expect(json['friendLastName'], tLastName);
expect(json['email'], tEmail);
expect(json['friendProfileImageUrl'], tImageUrl);
expect(json['status'], 'accepted');
expect(json['isOnline'], isTrue);
});
test('copyWith should create new instance with updated fields', () {
// Arrange
final original = Friend(
friendId: tFriendId,
friendFirstName: tFirstName,
friendLastName: tLastName,
status: FriendStatus.pending,
);
// Act
final updated = original.copyWith(
status: FriendStatus.accepted,
isOnline: true,
);
// Assert
expect(updated.friendId, original.friendId); // Unchanged
expect(updated.friendFirstName, original.friendFirstName); // Unchanged
expect(updated.status, FriendStatus.accepted); // Changed
expect(updated.isOnline, isTrue); // Changed
});
});
group('FriendStatus', () {
test('should have all required status values', () {
expect(FriendStatus.values.length, 4);
expect(FriendStatus.values, contains(FriendStatus.pending));
expect(FriendStatus.values, contains(FriendStatus.accepted));
expect(FriendStatus.values, contains(FriendStatus.blocked));
expect(FriendStatus.values, contains(FriendStatus.unknown));
});
test('parseStatus should handle different string formats', () {
// This tests the private method indirectly through fromJson
final jsonPending = {'friendId': '1', 'status': 'pending'};
final jsonAccepted = {'friendId': '1', 'status': 'accepted'};
final jsonBlocked = {'friendId': '1', 'status': 'blocked'};
final jsonUnknown = {'friendId': '1', 'status': 'invalid'};
expect(Friend.fromJson(jsonPending).status, FriendStatus.pending);
expect(Friend.fromJson(jsonAccepted).status, FriendStatus.accepted);
expect(Friend.fromJson(jsonBlocked).status, FriendStatus.blocked);
expect(Friend.fromJson(jsonUnknown).status, FriendStatus.unknown);
});
});
}

View File

@@ -0,0 +1,150 @@
import 'package:afterwork/domain/entities/user.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('User Entity', () {
const tUserId = '123';
const tUserLastName = 'Doe';
const tUserFirstName = 'John';
const tEmail = 'john.doe@example.com';
const tMotDePasse = 'hashedPassword123';
const tProfileImageUrl = 'https://example.com/profile.jpg';
const tEventsCount = 5;
const tFriendsCount = 10;
const tPostsCount = 15;
const tVisitedPlacesCount = 20;
test('should create a User instance with all required fields', () {
// Act
const user = User(
userId: tUserId,
userLastName: tUserLastName,
userFirstName: tUserFirstName,
email: tEmail,
motDePasse: tMotDePasse,
profileImageUrl: tProfileImageUrl,
);
// Assert
expect(user.userId, tUserId);
expect(user.userLastName, tUserLastName);
expect(user.userFirstName, tUserFirstName);
expect(user.email, tEmail);
expect(user.motDePasse, tMotDePasse);
expect(user.profileImageUrl, tProfileImageUrl);
expect(user.eventsCount, 0); // Default value
expect(user.friendsCount, 0); // Default value
expect(user.postsCount, 0); // Default value
expect(user.visitedPlacesCount, 0); // Default value
});
test('should create a User instance with optional counts', () {
// Act
const user = User(
userId: tUserId,
userLastName: tUserLastName,
userFirstName: tUserFirstName,
email: tEmail,
motDePasse: tMotDePasse,
profileImageUrl: tProfileImageUrl,
eventsCount: tEventsCount,
friendsCount: tFriendsCount,
postsCount: tPostsCount,
visitedPlacesCount: tVisitedPlacesCount,
);
// Assert
expect(user.eventsCount, tEventsCount);
expect(user.friendsCount, tFriendsCount);
expect(user.postsCount, tPostsCount);
expect(user.visitedPlacesCount, tVisitedPlacesCount);
});
test('should support value equality using Equatable', () {
// Arrange
const user1 = User(
userId: tUserId,
userLastName: tUserLastName,
userFirstName: tUserFirstName,
email: tEmail,
motDePasse: tMotDePasse,
profileImageUrl: tProfileImageUrl,
);
const user2 = User(
userId: tUserId,
userLastName: tUserLastName,
userFirstName: tUserFirstName,
email: tEmail,
motDePasse: tMotDePasse,
profileImageUrl: tProfileImageUrl,
);
// Assert
expect(user1, equals(user2));
expect(user1.hashCode, equals(user2.hashCode));
});
test('should not be equal when any field is different', () {
// Arrange
const user1 = User(
userId: tUserId,
userLastName: tUserLastName,
userFirstName: tUserFirstName,
email: tEmail,
motDePasse: tMotDePasse,
profileImageUrl: tProfileImageUrl,
);
const user2 = User(
userId: 'different-id',
userLastName: tUserLastName,
userFirstName: tUserFirstName,
email: tEmail,
motDePasse: tMotDePasse,
profileImageUrl: tProfileImageUrl,
);
// Assert
expect(user1, isNot(equals(user2)));
});
test('props should contain all fields for equality comparison', () {
// Arrange
const user = User(
userId: tUserId,
userLastName: tUserLastName,
userFirstName: tUserFirstName,
email: tEmail,
motDePasse: tMotDePasse,
profileImageUrl: tProfileImageUrl,
eventsCount: tEventsCount,
friendsCount: tFriendsCount,
postsCount: tPostsCount,
visitedPlacesCount: tVisitedPlacesCount,
);
// Act
final props = user.props;
// Assert
expect(props.length, 10);
expect(
props,
containsAll([
tUserId,
tUserLastName,
tUserFirstName,
tEmail,
tMotDePasse,
tProfileImageUrl,
tEventsCount,
tFriendsCount,
tPostsCount,
tVisitedPlacesCount,
]),
);
});
});
}