/// Tests unitaires pour SolidarityBloc library solidarity_bloc_test; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:unionflow_mobile_apps/features/solidarity/bloc/solidarity_bloc.dart'; import 'package:unionflow_mobile_apps/features/solidarity/data/models/demande_aide_model.dart'; import 'package:unionflow_mobile_apps/features/solidarity/data/repositories/demande_aide_repository.dart'; @GenerateMocks([DemandeAideRepository]) import 'solidarity_bloc_test.mocks.dart'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- DemandeAideModel _makeDemande({ String id = 'd1', String statut = 'EN_ATTENTE', String type = 'MEDICAL', }) => DemandeAideModel( id: id, titre: 'Aide médicale', description: 'Opération chirurgicale urgente', type: type, statut: statut, montantDemande: 500000, demandeurId: 'membre1', demandeur: 'Koné Awa', ); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { late MockDemandeAideRepository mockRepository; SolidarityBloc buildBloc() => SolidarityBloc(mockRepository); setUp(() { mockRepository = MockDemandeAideRepository(); }); // ---- initial state ------------------------------------------------------- test('initial state has status initial and empty demandes list', () { final bloc = buildBloc(); expect(bloc.state.status, SolidarityStatus.initial); expect(bloc.state.demandes, isEmpty); bloc.close(); }); // ---- LoadDemandesAide ---------------------------------------------------- group('LoadDemandesAide', () { final demandeList = [_makeDemande(), _makeDemande(id: 'd2')]; blocTest( 'emits loading then loaded with demandes', build: () { when(mockRepository.getMesDemandes( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => demandeList); return buildBloc(); }, act: (b) => b.add(const LoadDemandesAide()), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded) .having((s) => s.demandes.length, 'count', 2), ], ); blocTest( 'emits loading then loaded with empty list', build: () { when(mockRepository.getMesDemandes( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => []); return buildBloc(); }, act: (b) => b.add(const LoadDemandesAide()), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded) .having((s) => s.demandes, 'demandes', isEmpty), ], ); blocTest( 'emits loading then error on exception', build: () { when(mockRepository.getMesDemandes( page: anyNamed('page'), size: anyNamed('size'))) .thenThrow(Exception('network error')); return buildBloc(); }, act: (b) => b.add(const LoadDemandesAide()), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.error) .having((s) => s.error, 'error', isNotNull), ], ); blocTest( 'uses custom page and size', build: () { when(mockRepository.getMesDemandes(page: 1, size: 5)) .thenAnswer((_) async => [_makeDemande()]); return buildBloc(); }, act: (b) => b.add(const LoadDemandesAide(page: 1, size: 5)), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded), ], verify: (_) => verify(mockRepository.getMesDemandes(page: 1, size: 5)), ); }); // ---- LoadDemandeAideById ------------------------------------------------- group('LoadDemandeAideById', () { final demande = _makeDemande(); blocTest( 'emits loading then loaded with demandeDetail', build: () { when(mockRepository.getById(any)).thenAnswer((_) async => demande); return buildBloc(); }, act: (b) => b.add(LoadDemandeAideById('d1')), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded) .having((s) => s.demandeDetail?.id, 'detail id', 'd1'), ], ); blocTest( 'emits error when not found', build: () { when(mockRepository.getById(any)) .thenThrow(Exception('not found')); return buildBloc(); }, act: (b) => b.add(LoadDemandeAideById('missing')), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.error), ], ); blocTest( 'emits null when repository returns null', build: () { when(mockRepository.getById(any)).thenAnswer((_) async => null); return buildBloc(); }, act: (b) => b.add(LoadDemandeAideById('d1')), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded) .having((s) => s.demandeDetail, 'detail', isNull), ], ); }); // ---- SearchDemandesAide -------------------------------------------------- group('SearchDemandesAide', () { final results = [_makeDemande(type: 'MEDICAL')]; blocTest( 'emits loading then loaded with filtered results', build: () { when(mockRepository.search( statut: anyNamed('statut'), type: anyNamed('type'), page: anyNamed('page'), size: anyNamed('size'), )).thenAnswer((_) async => results); return buildBloc(); }, act: (b) => b.add(const SearchDemandesAide( statut: 'EN_ATTENTE', type: 'MEDICAL', )), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded) .having((s) => s.demandes.length, 'count', 1), ], verify: (_) => verify(mockRepository.search( statut: 'EN_ATTENTE', type: 'MEDICAL', page: anyNamed('page'), size: anyNamed('size'), )), ); blocTest( 'emits loading then error on failure', build: () { when(mockRepository.search( statut: anyNamed('statut'), type: anyNamed('type'), page: anyNamed('page'), size: anyNamed('size'), )).thenThrow(Exception('search failed')); return buildBloc(); }, act: (b) => b.add(const SearchDemandesAide(statut: 'APPROUVEE')), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.error), ], ); blocTest( 'searches without filters (null statut and type)', build: () { when(mockRepository.search( statut: anyNamed('statut'), type: anyNamed('type'), page: anyNamed('page'), size: anyNamed('size'), )).thenAnswer((_) async => results); return buildBloc(); }, act: (b) => b.add(const SearchDemandesAide()), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded), ], ); }); // ---- CreateDemandeAide --------------------------------------------------- group('CreateDemandeAide', () { final demande = _makeDemande(); blocTest( 'creates demande then re-triggers LoadDemandesAide', build: () { when(mockRepository.create(any)).thenAnswer((_) async => demande); when(mockRepository.getMesDemandes( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => [demande]); return buildBloc(); }, act: (b) => b.add(CreateDemandeAide(demande)), expect: () => [ // Loading from CreateDemandeAide isA() .having((s) => s.status, 'status', SolidarityStatus.loading), // Loading from auto-triggered LoadDemandesAide isA() .having((s) => s.status, 'status', SolidarityStatus.loading), // Loaded from LoadDemandesAide isA() .having((s) => s.status, 'status', SolidarityStatus.loaded), ], ); blocTest( 'emits error on creation failure', build: () { when(mockRepository.create(any)) .thenThrow(Exception('creation failed')); return buildBloc(); }, act: (b) => b.add(CreateDemandeAide(demande)), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.error), ], ); }); // ---- ApprouverDemandeAide ------------------------------------------------ group('ApprouverDemandeAide', () { final approved = _makeDemande(statut: 'APPROUVEE'); blocTest( 'approves demande and updates demandeDetail, re-triggers LoadDemandesAide', build: () { when(mockRepository.approuver(any)) .thenAnswer((_) async => approved); when(mockRepository.getMesDemandes( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => [approved]); return buildBloc(); }, act: (b) => b.add(ApprouverDemandeAide('d1')), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded) .having((s) => s.demandeDetail?.statut, 'statut', 'APPROUVEE'), isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded), ], ); blocTest( 'emits error on failure', build: () { when(mockRepository.approuver(any)) .thenThrow(Exception('approval failed')); return buildBloc(); }, act: (b) => b.add(ApprouverDemandeAide('d1')), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.error), ], ); }); // ---- RejeterDemandeAide -------------------------------------------------- group('RejeterDemandeAide', () { final rejected = _makeDemande(statut: 'REJETEE'); blocTest( 'rejects demande with motif and re-triggers LoadDemandesAide', build: () { when(mockRepository.rejeter(any, motif: anyNamed('motif'))) .thenAnswer((_) async => rejected); when(mockRepository.getMesDemandes( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => []); return buildBloc(); }, act: (b) => b.add(RejeterDemandeAide('d1', motif: 'Dossier incomplet')), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded) .having((s) => s.demandeDetail?.statut, 'statut', 'REJETEE'), isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded), ], ); blocTest( 'rejects without motif', build: () { when(mockRepository.rejeter(any, motif: anyNamed('motif'))) .thenAnswer((_) async => rejected); when(mockRepository.getMesDemandes( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => []); return buildBloc(); }, act: (b) => b.add(RejeterDemandeAide('d1')), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded), isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.loaded), ], ); blocTest( 'emits error on failure', build: () { when(mockRepository.rejeter(any, motif: anyNamed('motif'))) .thenThrow(Exception('rejection failed')); return buildBloc(); }, act: (b) => b.add(RejeterDemandeAide('d1', motif: 'motif')), expect: () => [ isA() .having((s) => s.status, 'status', SolidarityStatus.loading), isA() .having((s) => s.status, 'status', SolidarityStatus.error) .having((s) => s.message, 'message', isNotNull), ], ); }); // ---- copyWith / state preservation --------------------------------------- group('State preservation via copyWith', () { test('loaded state preserves existing demandes list on error', () async { final existingList = [_makeDemande(id: 'existing')]; when(mockRepository.getMesDemandes( page: anyNamed('page'), size: anyNamed('size'))) .thenAnswer((_) async => existingList); final bloc = buildBloc(); bloc.add(const LoadDemandesAide()); await Future.delayed(const Duration(milliseconds: 50)); expect(bloc.state.status, SolidarityStatus.loaded); expect(bloc.state.demandes.length, 1); when(mockRepository.getById(any)) .thenThrow(Exception('lookup failed')); bloc.add(LoadDemandeAideById('missing')); await Future.delayed(const Duration(milliseconds: 50)); // demandes list should still be preserved expect(bloc.state.demandes.length, 1); bloc.close(); }); }); }