## 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
396 lines
12 KiB
Dart
396 lines
12 KiB
Dart
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
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|