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:
395
test/presentation/state_management/event_bloc_test.dart
Normal file
395
test/presentation/state_management/event_bloc_test.dart
Normal file
@@ -0,0 +1,395 @@
|
||||
import 'package:afterwork/data/datasources/event_remote_data_source.dart';
|
||||
import 'package:afterwork/data/models/event_model.dart';
|
||||
import 'package:afterwork/presentation/state_management/event_bloc.dart';
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
// Mock classes
|
||||
class MockEventRemoteDataSource extends Mock
|
||||
implements EventRemoteDataSource {}
|
||||
|
||||
void main() {
|
||||
late EventBloc eventBloc;
|
||||
late MockEventRemoteDataSource mockRemoteDataSource;
|
||||
|
||||
setUpAll(() {
|
||||
// Enregistrer fallback value pour EventModel
|
||||
registerFallbackValue(
|
||||
EventModel(
|
||||
id: 'fallback',
|
||||
title: 'Fallback',
|
||||
description: 'Fallback',
|
||||
startDate: '2026-01-01T00:00:00Z',
|
||||
location: 'Fallback',
|
||||
category: 'Fallback',
|
||||
link: 'Fallback',
|
||||
creatorEmail: 'fallback@example.com',
|
||||
creatorFirstName: 'Fallback',
|
||||
creatorLastName: 'Fallback',
|
||||
profileImageUrl: 'fallback.jpg',
|
||||
participants: const [],
|
||||
status: 'ouvert',
|
||||
reactionsCount: 0,
|
||||
commentsCount: 0,
|
||||
sharesCount: 0,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
mockRemoteDataSource = MockEventRemoteDataSource();
|
||||
eventBloc = EventBloc(remoteDataSource: mockRemoteDataSource);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
eventBloc.close();
|
||||
});
|
||||
|
||||
final tEventModels = [
|
||||
EventModel(
|
||||
id: '1',
|
||||
title: 'Event 1',
|
||||
description: 'Description 1',
|
||||
startDate: '2026-01-15T19:00:00Z',
|
||||
location: 'Location 1',
|
||||
category: 'Category 1',
|
||||
link: 'https://link1.com',
|
||||
creatorEmail: 'creator1@example.com',
|
||||
creatorFirstName: 'John',
|
||||
creatorLastName: 'Doe',
|
||||
profileImageUrl: 'profile1.jpg',
|
||||
participants: const [],
|
||||
status: 'ouvert',
|
||||
reactionsCount: 0,
|
||||
commentsCount: 0,
|
||||
sharesCount: 0,
|
||||
),
|
||||
EventModel(
|
||||
id: '2',
|
||||
title: 'Event 2',
|
||||
description: 'Description 2',
|
||||
startDate: '2026-01-16T19:00:00Z',
|
||||
location: 'Location 2',
|
||||
category: 'Category 2',
|
||||
link: 'https://link2.com',
|
||||
creatorEmail: 'creator2@example.com',
|
||||
creatorFirstName: 'Jane',
|
||||
creatorLastName: 'Smith',
|
||||
profileImageUrl: 'profile2.jpg',
|
||||
participants: const [],
|
||||
status: 'ouvert',
|
||||
reactionsCount: 0,
|
||||
commentsCount: 0,
|
||||
sharesCount: 0,
|
||||
),
|
||||
];
|
||||
|
||||
group('EventBloc', () {
|
||||
test('initial state should be EventInitial', () {
|
||||
// Assert
|
||||
expect(eventBloc.state, isA<EventInitial>());
|
||||
});
|
||||
|
||||
group('LoadEvents', () {
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should emit [EventLoading, EventLoaded] when data is gotten successfully',
|
||||
build: () {
|
||||
when(
|
||||
() => mockRemoteDataSource
|
||||
.getEventsCreatedByUserAndFriends(any()),
|
||||
).thenAnswer((_) async => tEventModels);
|
||||
return eventBloc;
|
||||
},
|
||||
act: (bloc) => bloc.add(const LoadEvents('user123')),
|
||||
expect: () => [
|
||||
isA<EventLoading>(),
|
||||
isA<EventLoaded>().having((s) => s.events, 'events', tEventModels),
|
||||
],
|
||||
verify: (_) {
|
||||
verify(
|
||||
() => mockRemoteDataSource
|
||||
.getEventsCreatedByUserAndFriends('user123'),
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should emit [EventLoading, EventError] when getting data fails',
|
||||
build: () {
|
||||
when(
|
||||
() => mockRemoteDataSource
|
||||
.getEventsCreatedByUserAndFriends(any()),
|
||||
).thenThrow(Exception('Failed to load events'));
|
||||
return eventBloc;
|
||||
},
|
||||
act: (bloc) => bloc.add(const LoadEvents('user123')),
|
||||
expect: () => [
|
||||
isA<EventLoading>(),
|
||||
isA<EventError>()
|
||||
.having((s) => s.message, 'message', 'Erreur lors du chargement des événements.'),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should emit empty list when no events found',
|
||||
build: () {
|
||||
when(
|
||||
() => mockRemoteDataSource
|
||||
.getEventsCreatedByUserAndFriends(any()),
|
||||
).thenAnswer((_) async => []);
|
||||
return eventBloc;
|
||||
},
|
||||
act: (bloc) => bloc.add(const LoadEvents('user123')),
|
||||
expect: () => [
|
||||
isA<EventLoading>(),
|
||||
isA<EventLoaded>().having((s) => s.events, 'events', isEmpty),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
group('AddEvent', () {
|
||||
final tNewEvent = EventModel(
|
||||
id: '3',
|
||||
title: 'New Event',
|
||||
description: 'New Description',
|
||||
startDate: '2026-01-17T19:00:00Z',
|
||||
location: 'New Location',
|
||||
category: 'New Category',
|
||||
link: 'https://newlink.com',
|
||||
creatorEmail: 'new@example.com',
|
||||
creatorFirstName: 'New',
|
||||
creatorLastName: 'Creator',
|
||||
profileImageUrl: 'newprofile.jpg',
|
||||
participants: const [],
|
||||
status: 'ouvert',
|
||||
reactionsCount: 0,
|
||||
commentsCount: 0,
|
||||
sharesCount: 0,
|
||||
);
|
||||
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should emit [EventLoading, EventLoaded] when event is added successfully',
|
||||
build: () {
|
||||
when(() => mockRemoteDataSource.createEvent(any()))
|
||||
.thenAnswer((_) async => tNewEvent);
|
||||
when(() => mockRemoteDataSource.getAllEvents())
|
||||
.thenAnswer((_) async => [...tEventModels, tNewEvent]);
|
||||
return eventBloc;
|
||||
},
|
||||
act: (bloc) => bloc.add(AddEvent(tNewEvent)),
|
||||
expect: () => [
|
||||
isA<EventLoading>(),
|
||||
isA<EventLoaded>()
|
||||
.having((s) => s.events.length, 'events.length', 3)
|
||||
.having((s) => s.events.last.id, 'last event id', '3'),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should emit [EventLoading, EventError] when adding event fails',
|
||||
build: () {
|
||||
when(() => mockRemoteDataSource.createEvent(any()))
|
||||
.thenThrow(Exception('Failed to create event'));
|
||||
return eventBloc;
|
||||
},
|
||||
act: (bloc) => bloc.add(AddEvent(tNewEvent)),
|
||||
expect: () => [
|
||||
isA<EventLoading>(),
|
||||
isA<EventError>()
|
||||
.having((s) => s.message, 'message', "Erreur lors de l'ajout de l'événement."),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
group('CloseEvent', () {
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should emit [EventLoading, EventLoaded] with updated event status',
|
||||
build: () {
|
||||
when(() => mockRemoteDataSource.closeEvent(any()))
|
||||
.thenAnswer((_) async => {});
|
||||
return eventBloc;
|
||||
},
|
||||
seed: () => EventLoaded(tEventModels),
|
||||
act: (bloc) => bloc.add(const CloseEvent('1')),
|
||||
wait: const Duration(milliseconds: 100),
|
||||
expect: () => [
|
||||
isA<EventLoading>(),
|
||||
predicate<EventState>((state) {
|
||||
if (state is EventLoaded) {
|
||||
try {
|
||||
final event = state.events.firstWhere((e) => e.id == '1');
|
||||
return event.status == 'fermé';
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should emit [EventLoading, EventError] when closing event fails',
|
||||
build: () {
|
||||
when(() => mockRemoteDataSource.closeEvent(any()))
|
||||
.thenThrow(Exception('Failed to close event'));
|
||||
return eventBloc;
|
||||
},
|
||||
seed: () => EventLoaded(tEventModels),
|
||||
act: (bloc) => bloc.add(const CloseEvent('1')),
|
||||
expect: () => [
|
||||
isA<EventLoading>(),
|
||||
isA<EventError>()
|
||||
.having((s) => s.message, 'message', "Erreur lors de la fermeture de l'événement."),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should not update state when event is not in loaded list',
|
||||
build: () {
|
||||
when(() => mockRemoteDataSource.closeEvent(any()))
|
||||
.thenAnswer((_) async => {});
|
||||
return eventBloc;
|
||||
},
|
||||
seed: () => EventLoaded(tEventModels),
|
||||
act: (bloc) => bloc.add(const CloseEvent('non-existent-id')),
|
||||
expect: () => [
|
||||
isA<EventLoading>(),
|
||||
isA<EventLoaded>(), // Restaure l'état précédent
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
group('ReopenEvent', () {
|
||||
final closedEvents = tEventModels.map((e) {
|
||||
final event = EventModel(
|
||||
id: e.id,
|
||||
title: e.title,
|
||||
description: e.description,
|
||||
startDate: e.startDate,
|
||||
location: e.location,
|
||||
category: e.category,
|
||||
link: e.link,
|
||||
creatorEmail: e.creatorEmail,
|
||||
creatorFirstName: e.creatorFirstName,
|
||||
creatorLastName: e.creatorLastName,
|
||||
profileImageUrl: e.profileImageUrl,
|
||||
participants: e.participants,
|
||||
status: 'fermé',
|
||||
reactionsCount: e.reactionsCount,
|
||||
commentsCount: e.commentsCount,
|
||||
sharesCount: e.sharesCount,
|
||||
);
|
||||
return event;
|
||||
}).toList();
|
||||
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should emit [EventLoading, EventLoaded] with reopened event',
|
||||
build: () {
|
||||
when(() => mockRemoteDataSource.reopenEvent(any()))
|
||||
.thenAnswer((_) async => {});
|
||||
return eventBloc;
|
||||
},
|
||||
seed: () => EventLoaded(closedEvents),
|
||||
act: (bloc) => bloc.add(const ReopenEvent('1')),
|
||||
wait: const Duration(milliseconds: 100),
|
||||
expect: () => [
|
||||
isA<EventLoading>(),
|
||||
predicate<EventState>((state) {
|
||||
if (state is EventLoaded) {
|
||||
try {
|
||||
final event = state.events.firstWhere((e) => e.id == '1');
|
||||
return event.status == 'ouvert';
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should emit [EventLoading, EventError] when reopening event fails',
|
||||
build: () {
|
||||
when(() => mockRemoteDataSource.reopenEvent(any()))
|
||||
.thenThrow(Exception('Failed to reopen event'));
|
||||
return eventBloc;
|
||||
},
|
||||
seed: () => EventLoaded(closedEvents),
|
||||
act: (bloc) => bloc.add(const ReopenEvent('1')),
|
||||
expect: () => [
|
||||
isA<EventLoading>(),
|
||||
isA<EventError>()
|
||||
.having((s) => s.message, 'message', "Erreur lors de la réouverture de l'événement."),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should not update state when reopening non-existent event',
|
||||
build: () {
|
||||
when(() => mockRemoteDataSource.reopenEvent(any()))
|
||||
.thenAnswer((_) async => {});
|
||||
return eventBloc;
|
||||
},
|
||||
seed: () => EventLoaded(closedEvents),
|
||||
act: (bloc) => bloc.add(const ReopenEvent('non-existent-id')),
|
||||
expect: () => [
|
||||
isA<EventLoading>(),
|
||||
isA<EventLoaded>(), // Restaure l'état précédent
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
group('RemoveEvent', () {
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should remove event from list locally',
|
||||
build: () => eventBloc,
|
||||
seed: () => EventLoaded(tEventModels),
|
||||
act: (bloc) => bloc.add(const RemoveEvent('1')),
|
||||
skip: 0,
|
||||
expect: () => [
|
||||
isA<EventLoaded>().having(
|
||||
(s) => s.events.length,
|
||||
'events.length',
|
||||
1,
|
||||
).having(
|
||||
(s) => s.events.first.id,
|
||||
'first event id',
|
||||
'2',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<EventBloc, EventState>(
|
||||
'should not emit anything when state is not EventLoaded',
|
||||
build: () => eventBloc,
|
||||
seed: () => const EventInitial(),
|
||||
act: (bloc) => bloc.add(const RemoveEvent('1')),
|
||||
expect: () => [],
|
||||
);
|
||||
|
||||
test('should handle removing non-existent event gracefully', () async {
|
||||
// Arrange
|
||||
final bloc = eventBloc;
|
||||
final initialEvents = tEventModels;
|
||||
|
||||
// Act
|
||||
bloc.emit(EventLoaded(initialEvents));
|
||||
bloc.add(const RemoveEvent('non-existent-id'));
|
||||
|
||||
// Wait for the event to be processed
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
// Assert
|
||||
final currentState = bloc.state;
|
||||
expect(currentState, isA<EventLoaded>());
|
||||
if (currentState is EventLoaded) {
|
||||
expect(currentState.events.length, 2); // La liste reste inchangée
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user