import 'package:bloc_test/bloc_test.dart'; import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:unionflow_mobile_apps/features/organizations/bloc/organizations_bloc.dart'; import 'package:unionflow_mobile_apps/features/organizations/bloc/organizations_event.dart'; import 'package:unionflow_mobile_apps/features/organizations/bloc/organizations_state.dart'; import 'package:unionflow_mobile_apps/features/organizations/data/models/organization_model.dart'; import 'package:unionflow_mobile_apps/features/organizations/data/services/organization_service.dart'; import 'package:unionflow_mobile_apps/features/organizations/domain/repositories/organization_repository.dart'; import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/create_organization.dart' as uc; import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/delete_organization.dart' as uc; import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_organization_by_id.dart'; import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/get_organizations.dart'; import 'package:unionflow_mobile_apps/features/organizations/domain/usecases/update_organization.dart' as uc; @GenerateMocks([ GetOrganizations, GetOrganizationById, uc.CreateOrganization, uc.UpdateOrganization, uc.DeleteOrganization, IOrganizationRepository, OrganizationService, ]) import 'organizations_bloc_test.mocks.dart'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- OrganizationModel _makeOrg({ String id = 'org-1', String nom = 'Association Test', StatutOrganization statut = StatutOrganization.active, String type = 'ASSOCIATION', int nombreMembres = 10, }) => OrganizationModel( id: id, nom: nom, typeOrganisation: type, statut: statut, nombreMembres: nombreMembres, ); // --------------------------------------------------------------------------- void main() { late MockGetOrganizations mockGetOrganizations; late MockGetOrganizationById mockGetOrganizationById; late MockCreateOrganization mockCreateOrganization; late MockUpdateOrganization mockUpdateOrganization; late MockDeleteOrganization mockDeleteOrganization; late MockIOrganizationRepository mockRepository; late MockOrganizationService mockOrganizationService; OrganizationsBloc buildBloc() => OrganizationsBloc( mockGetOrganizations, mockGetOrganizationById, mockCreateOrganization, mockUpdateOrganization, mockDeleteOrganization, mockRepository, mockOrganizationService, ); setUp(() { mockGetOrganizations = MockGetOrganizations(); mockGetOrganizationById = MockGetOrganizationById(); mockCreateOrganization = MockCreateOrganization(); mockUpdateOrganization = MockUpdateOrganization(); mockDeleteOrganization = MockDeleteOrganization(); mockRepository = MockIOrganizationRepository(); mockOrganizationService = MockOrganizationService(); // Default stubs for searchLocal / filterByStatus / filterByType when(mockOrganizationService.searchLocal(any, any)).thenAnswer( (invocation) => invocation.positionalArguments[0] as List, ); when(mockOrganizationService.filterByStatus(any, any)).thenAnswer( (invocation) => invocation.positionalArguments[0] as List, ); when(mockOrganizationService.filterByType(any, any)).thenAnswer( (invocation) => invocation.positionalArguments[0] as List, ); when(mockOrganizationService.sortByName(any, ascending: anyNamed('ascending'))).thenAnswer( (invocation) => invocation.positionalArguments[0] as List, ); when(mockOrganizationService.sortByCreationDate(any, ascending: anyNamed('ascending'))).thenAnswer( (invocation) => invocation.positionalArguments[0] as List, ); when(mockOrganizationService.sortByMemberCount(any, ascending: anyNamed('ascending'))).thenAnswer( (invocation) => invocation.positionalArguments[0] as List, ); }); // ─── Initial state ──────────────────────────────────────────────────────── group('initial state', () { test('is OrganizationsInitial', () { expect(buildBloc().state, const OrganizationsInitial()); }); }); // ─── LoadOrganizations ──────────────────────────────────────────────────── group('LoadOrganizations', () { final orgs = [_makeOrg(), _makeOrg(id: 'org-2', nom: 'Mutuelle B')]; blocTest( 'emits [Loading, Loaded] on success', build: buildBloc, setUp: () => when(mockGetOrganizations( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenAnswer((_) async => orgs), act: (b) => b.add(const LoadOrganizations()), expect: () => [ const OrganizationsLoading(), isA() .having((s) => s.organizations, 'organizations', orgs) .having((s) => s.currentPage, 'page', 0), ], ); blocTest( 'emits [Loading, Error] on exception', build: buildBloc, setUp: () => when(mockGetOrganizations( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenThrow(Exception('network error')), act: (b) => b.add(const LoadOrganizations()), expect: () => [ const OrganizationsLoading(), isA() .having((e) => e.message, 'message', contains('chargement des organisations')), ], ); blocTest( 'uses useMesOnly endpoint when flag is set', build: buildBloc, setUp: () => when(mockRepository.getMesOrganisations()).thenAnswer((_) async => orgs), act: (b) => b.add(const LoadOrganizations(useMesOnly: true)), expect: () => [ const OrganizationsLoading(), isA().having((s) => s.useMesOnly, 'useMesOnly', true), ], ); blocTest( 'filters by filterOrganizationIds when provided', build: buildBloc, setUp: () => when(mockGetOrganizations( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenAnswer((_) async => orgs), act: (b) => b.add(const LoadOrganizations(filterOrganizationIds: ['org-1'])), expect: () => [ const OrganizationsLoading(), isA() .having((s) => s.organizations.length, 'count', 1) .having((s) => s.organizations.first.id, 'id', 'org-1'), ], ); blocTest( 'does NOT emit Loading when state is already Loaded and refresh=false', build: buildBloc, seed: () => OrganizationsLoaded( organizations: orgs, filteredOrganizations: orgs, ), setUp: () => when(mockGetOrganizations( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenAnswer((_) async => orgs), act: (b) => b.add(const LoadOrganizations()), expect: () => [ isA(), ], ); }); // ─── LoadMoreOrganizations ──────────────────────────────────────────────── group('LoadMoreOrganizations', () { final initialOrgs = [_makeOrg()]; final moreOrgs = [_makeOrg(id: 'org-2', nom: 'Org B')]; final loadedState = OrganizationsLoaded( organizations: initialOrgs, filteredOrganizations: initialOrgs, hasReachedMax: false, currentPage: 0, ); blocTest( 'emits [LoadingMore, Loaded] with appended orgs', build: buildBloc, seed: () => loadedState, setUp: () => when(mockGetOrganizations( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenAnswer((_) async => moreOrgs), act: (b) => b.add(const LoadMoreOrganizations()), expect: () => [ isA(), isA() .having((s) => s.organizations.length, 'total', 2) .having((s) => s.currentPage, 'page', 1), ], ); blocTest( 'does nothing when hasReachedMax is true', build: buildBloc, seed: () => OrganizationsLoaded( organizations: initialOrgs, filteredOrganizations: initialOrgs, hasReachedMax: true, ), act: (b) => b.add(const LoadMoreOrganizations()), expect: () => [], ); blocTest( 'does nothing when state is not OrganizationsLoaded', build: buildBloc, act: (b) => b.add(const LoadMoreOrganizations()), expect: () => [], ); blocTest( 'sets hasReachedMax when fewer than 20 items returned', build: buildBloc, seed: () => loadedState, setUp: () => when(mockGetOrganizations( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenAnswer((_) async => moreOrgs), act: (b) => b.add(const LoadMoreOrganizations()), expect: () => [ isA(), isA().having((s) => s.hasReachedMax, 'hasReachedMax', true), ], ); blocTest( 'emits error with previousOrganizations on failure', build: buildBloc, seed: () => loadedState, setUp: () => when(mockGetOrganizations( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenThrow(Exception('network')), act: (b) => b.add(const LoadMoreOrganizations()), expect: () => [ isA(), isA() .having((e) => e.previousOrganizations, 'previousOrgs', isNotNull), ], ); }); // ─── SearchOrganizations ────────────────────────────────────────────────── group('SearchOrganizations', () { final orgs = [_makeOrg(), _makeOrg(id: 'org-2', nom: 'TONTINE Test')]; final loadedState = OrganizationsLoaded( organizations: orgs, filteredOrganizations: orgs, ); blocTest( 'clears search when query is empty (with existing search)', build: buildBloc, seed: () => OrganizationsLoaded( organizations: orgs, filteredOrganizations: orgs, currentSearch: 'previous query', ), act: (b) => b.add(const SearchOrganizations('')), expect: () => [ isA() .having((s) => s.currentSearch, 'currentSearch', isNull), ], ); blocTest( 'performs local search then server search for non-empty query', build: buildBloc, seed: () => loadedState, setUp: () { when(mockOrganizationService.searchLocal(any, any)) .thenReturn([orgs.first]); when(mockGetOrganizations( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenAnswer((_) async => [orgs.first]); }, act: (b) => b.add(const SearchOrganizations('Association')), expect: () => [ isA() .having((s) => s.currentSearch, 'search', 'Association'), isA() .having((s) => s.hasReachedMax, 'hasReachedMax', true), ], ); blocTest( 'dispatches LoadOrganizations when not in loaded state', build: buildBloc, setUp: () => when(mockGetOrganizations( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenAnswer((_) async => orgs), act: (b) => b.add(const SearchOrganizations('test')), expect: () => [ const OrganizationsLoading(), isA(), ], ); }); // ─── AdvancedSearchOrganizations ────────────────────────────────────────── group('AdvancedSearchOrganizations', () { final orgs = [_makeOrg()]; blocTest( 'emits [Loading, Loaded] on success', build: buildBloc, setUp: () => when(mockRepository.searchOrganizations( nom: anyNamed('nom'), type: anyNamed('type'), statut: anyNamed('statut'), ville: anyNamed('ville'), region: anyNamed('region'), pays: anyNamed('pays'), page: anyNamed('page'), size: anyNamed('size'), )).thenAnswer((_) async => orgs), act: (b) => b.add(const AdvancedSearchOrganizations(nom: 'Test')), expect: () => [ const OrganizationsLoading(), isA(), ], ); blocTest( 'emits [Loading, Error] on failure', build: buildBloc, setUp: () => when(mockRepository.searchOrganizations( nom: anyNamed('nom'), type: anyNamed('type'), statut: anyNamed('statut'), ville: anyNamed('ville'), region: anyNamed('region'), pays: anyNamed('pays'), page: anyNamed('page'), size: anyNamed('size'), )).thenThrow(Exception('server error')), act: (b) => b.add(const AdvancedSearchOrganizations(nom: 'Test')), expect: () => [ const OrganizationsLoading(), isA(), ], ); }); // ─── LoadOrganizationById ───────────────────────────────────────────────── group('LoadOrganizationById', () { final org = _makeOrg(); blocTest( 'emits [OrganizationLoading, OrganizationLoaded] on success', build: buildBloc, setUp: () => when(mockGetOrganizationById('org-1')).thenAnswer((_) async => org), act: (b) => b.add(const LoadOrganizationById('org-1')), expect: () => [ const OrganizationLoading('org-1'), OrganizationLoaded(org), ], ); blocTest( 'emits [OrganizationLoading, OrganizationError] when not found (null)', build: buildBloc, setUp: () => when(mockGetOrganizationById('missing')).thenAnswer((_) async => null), act: (b) => b.add(const LoadOrganizationById('missing')), expect: () => [ const OrganizationLoading('missing'), isA().having((e) => e.organizationId, 'id', 'missing'), ], ); blocTest( 'emits [OrganizationLoading, OrganizationError] on exception', build: buildBloc, setUp: () => when(mockGetOrganizationById('org-1')).thenThrow(Exception('fail')), act: (b) => b.add(const LoadOrganizationById('org-1')), expect: () => [ const OrganizationLoading('org-1'), isA(), ], ); }); // ─── CreateOrganization ─────────────────────────────────────────────────── group('CreateOrganization', () { final newOrg = _makeOrg(id: 'new-1', nom: 'New Org'); blocTest( 'emits [Creating, Created] then triggers refresh on success', build: buildBloc, setUp: () { when(mockCreateOrganization(any)).thenAnswer((_) async => newOrg); when(mockGetOrganizations( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenAnswer((_) async => [newOrg]); }, act: (b) => b.add(CreateOrganization(newOrg)), expect: () => [ const OrganizationCreating(), OrganizationCreated(newOrg), const OrganizationsLoading(), isA(), ], ); blocTest( 'emits [Creating, Error] on failure', build: buildBloc, setUp: () => when(mockCreateOrganization(any)).thenThrow(Exception('conflict')), act: (b) => b.add(CreateOrganization(newOrg)), expect: () => [ const OrganizationCreating(), isA() .having((e) => e.message, 'message', contains('création')), ], ); }); // ─── UpdateOrganization ─────────────────────────────────────────────────── group('UpdateOrganization', () { final org = _makeOrg(); final updated = _makeOrg(nom: 'Updated'); final loadedState = OrganizationsLoaded( organizations: [org], filteredOrganizations: [org], ); blocTest( 'emits [Updating, Updated] on success (previousState captured after first emit)', build: buildBloc, seed: () => loadedState, setUp: () => when(mockUpdateOrganization('org-1', any)).thenAnswer((_) async => updated), act: (b) => b.add(UpdateOrganization('org-1', updated)), expect: () => [ const OrganizationUpdating('org-1'), OrganizationUpdated(updated), // Note: the BLoC captures previousState AFTER the first emit, so // previousState is OrganizationUpdating, not OrganizationsLoaded. // The list-update branch is only reached when starting from a loaded state // without any preceding emit — the inline emit order prevents it here. ], ); blocTest( 'emits [Updating, Error] on failure', build: buildBloc, setUp: () => when(mockUpdateOrganization(any, any)).thenThrow(Exception('fail')), act: (b) => b.add(UpdateOrganization('org-1', org)), expect: () => [ const OrganizationUpdating('org-1'), isA(), ], ); }); // ─── DeleteOrganization ─────────────────────────────────────────────────── group('DeleteOrganization', () { final org = _makeOrg(); final loadedState = OrganizationsLoaded( organizations: [org], filteredOrganizations: [org], ); blocTest( 'emits [Deleting, Deleted] on success (previousState captured after first emit)', build: buildBloc, seed: () => loadedState, setUp: () => when(mockDeleteOrganization('org-1')).thenAnswer((_) async {}), act: (b) => b.add(const DeleteOrganization('org-1')), expect: () => [ const OrganizationDeleting('org-1'), const OrganizationDeleted('org-1'), // Note: the BLoC captures previousState AFTER the first emit, // so the list-removal branch (previousState is OrganizationsLoaded) // is not triggered here — consistent with UpdateOrganization behavior. ], ); blocTest( 'emits [Deleting, Error] on failure', build: buildBloc, setUp: () => when(mockDeleteOrganization(any)).thenThrow(Exception('not found')), act: (b) => b.add(const DeleteOrganization('org-1')), expect: () => [ const OrganizationDeleting('org-1'), isA(), ], ); }); // ─── ActivateOrganization ───────────────────────────────────────────────── group('ActivateOrganization', () { final suspended = _makeOrg(statut: StatutOrganization.suspendue); final activated = _makeOrg(statut: StatutOrganization.active); final loadedState = OrganizationsLoaded( organizations: [suspended], filteredOrganizations: [suspended], ); blocTest( 'emits [Activating, Activated, Loaded(updated)] on success', build: buildBloc, seed: () => loadedState, setUp: () => when(mockRepository.activateOrganization('org-1')) .thenAnswer((_) async => activated), act: (b) => b.add(const ActivateOrganization('org-1')), expect: () => [ const OrganizationActivating('org-1'), OrganizationActivated(activated), isA() .having((s) => s.organizations.first.statut, 'statut', StatutOrganization.active), ], ); blocTest( 'emits [Activating, Error] on failure', build: buildBloc, setUp: () => when(mockRepository.activateOrganization(any)).thenThrow(Exception('fail')), act: (b) => b.add(const ActivateOrganization('org-1')), expect: () => [ const OrganizationActivating('org-1'), isA(), ], ); }); // ─── SuspendOrganization ────────────────────────────────────────────────── group('SuspendOrganization', () { final active = _makeOrg(); final suspended = _makeOrg(statut: StatutOrganization.suspendue); final loadedState = OrganizationsLoaded( organizations: [active], filteredOrganizations: [active], ); blocTest( 'emits [Suspending, Suspended, Loaded(updated)] on success', build: buildBloc, seed: () => loadedState, setUp: () => when(mockRepository.suspendOrganization('org-1')) .thenAnswer((_) async => suspended), act: (b) => b.add(const SuspendOrganization('org-1')), expect: () => [ const OrganizationSuspending('org-1'), OrganizationSuspended(suspended), isA() .having((s) => s.organizations.first.statut, 'statut', StatutOrganization.suspendue), ], ); blocTest( 'emits [Suspending, Error] on failure', build: buildBloc, setUp: () => when(mockRepository.suspendOrganization(any)).thenThrow(Exception('fail')), act: (b) => b.add(const SuspendOrganization('org-1')), expect: () => [ const OrganizationSuspending('org-1'), isA(), ], ); }); // ─── Filter & Sort ──────────────────────────────────────────────────────── group('FilterOrganizationsByStatus', () { final orgs = [ _makeOrg(statut: StatutOrganization.active), _makeOrg(id: 'org-2', statut: StatutOrganization.suspendue), ]; final loadedState = OrganizationsLoaded( organizations: orgs, filteredOrganizations: orgs, ); blocTest( 'emits updated filtered list', build: buildBloc, seed: () => loadedState, setUp: () => when(mockOrganizationService.filterByStatus(any, StatutOrganization.active)) .thenReturn([orgs.first]), act: (b) => b.add(const FilterOrganizationsByStatus(StatutOrganization.active)), expect: () => [ isA() .having((s) => s.statusFilter, 'filter', StatutOrganization.active), ], ); blocTest( 'does nothing when state is not loaded', build: buildBloc, act: (b) => b.add(const FilterOrganizationsByStatus(StatutOrganization.active)), expect: () => [], ); }); group('FilterOrganizationsByType', () { final orgs = [_makeOrg(), _makeOrg(id: 'org-2', type: 'TONTINE')]; final loadedState = OrganizationsLoaded( organizations: orgs, filteredOrganizations: orgs, ); blocTest( 'emits updated filtered list with typeFilter', build: buildBloc, seed: () => loadedState, setUp: () => when(mockOrganizationService.filterByType(any, 'TONTINE')).thenReturn([orgs[1]]), act: (b) => b.add(const FilterOrganizationsByType('TONTINE')), expect: () => [ isA() .having((s) => s.typeFilter, 'typeFilter', 'TONTINE'), ], ); }); group('SortOrganizations', () { final orgs = [_makeOrg(), _makeOrg(id: 'org-2', nom: 'AAA')]; final loadedState = OrganizationsLoaded( organizations: orgs, filteredOrganizations: orgs, ); blocTest( 'sorts by name ascending', build: buildBloc, seed: () => loadedState, setUp: () => when(mockOrganizationService.sortByName(any, ascending: anyNamed('ascending'))) .thenReturn([orgs[1], orgs[0]]), act: (b) => b.add(const SortOrganizations(OrganizationSortType.name)), expect: () => [ isA() .having((s) => s.sortType, 'sortType', OrganizationSortType.name), ], ); blocTest( 'sorts by creation date', build: buildBloc, seed: () => loadedState, setUp: () => when(mockOrganizationService.sortByCreationDate(any, ascending: anyNamed('ascending'))) .thenReturn(orgs), act: (b) => b.add(const SortOrganizations(OrganizationSortType.creationDate)), expect: () => [ isA() .having((s) => s.sortType, 'sortType', OrganizationSortType.creationDate), ], ); blocTest( 'sorts by member count', build: buildBloc, seed: () => loadedState, setUp: () => when(mockOrganizationService.sortByMemberCount(any, ascending: anyNamed('ascending'))) .thenReturn(orgs), act: (b) => b.add(const SortOrganizations(OrganizationSortType.memberCount)), expect: () => [ isA() .having((s) => s.sortType, 'sortType', OrganizationSortType.memberCount), ], ); blocTest( 'does nothing when state is not loaded', build: buildBloc, act: (b) => b.add(const SortOrganizations(OrganizationSortType.name)), expect: () => [], ); }); // ─── ClearOrganizationsFilters ──────────────────────────────────────────── group('ClearOrganizationsFilters', () { final orgs = [_makeOrg()]; final loadedState = OrganizationsLoaded( organizations: orgs, filteredOrganizations: orgs, statusFilter: StatutOrganization.active, typeFilter: 'TONTINE', currentSearch: 'some search', ); blocTest( 'clears all filters and restores full list', build: buildBloc, seed: () => loadedState, act: (b) => b.add(const ClearOrganizationsFilters()), expect: () => [ isA() .having((s) => s.statusFilter, 'statusFilter', isNull) .having((s) => s.typeFilter, 'typeFilter', isNull) .having((s) => s.currentSearch, 'search', isNull), ], ); blocTest( 'does nothing when state is not loaded', build: buildBloc, act: (b) => b.add(const ClearOrganizationsFilters()), expect: () => [], ); }); // ─── LoadOrganizationsStats ─────────────────────────────────────────────── group('LoadOrganizationsStats', () { final stats = {'total': 5, 'actives': 3}; blocTest( 'emits [StatsLoading, StatsLoaded] on success', build: buildBloc, setUp: () => when(mockRepository.getOrganizationsStats()).thenAnswer((_) async => stats), act: (b) => b.add(const LoadOrganizationsStats()), expect: () => [ const OrganizationsStatsLoading(), OrganizationsStatsLoaded(stats), ], ); blocTest( 'emits [StatsLoading, StatsError] on failure', build: buildBloc, setUp: () => when(mockRepository.getOrganizationsStats()).thenThrow(Exception('fail')), act: (b) => b.add(const LoadOrganizationsStats()), expect: () => [ const OrganizationsStatsLoading(), isA(), ], ); }); // ─── RefreshOrganizations ───────────────────────────────────────────────── group('RefreshOrganizations', () { final orgs = [_makeOrg()]; blocTest( 'triggers reload via LoadOrganizations with useMesOnly when applicable', build: buildBloc, seed: () => OrganizationsLoaded( organizations: orgs, filteredOrganizations: orgs, useMesOnly: true, ), setUp: () => when(mockRepository.getMesOrganisations()).thenAnswer((_) async => orgs), act: (b) => b.add(const RefreshOrganizations()), expect: () => [ const OrganizationsLoading(), isA().having((s) => s.useMesOnly, 'useMesOnly', true), ], ); blocTest( 'triggers standard reload from initial state', build: buildBloc, setUp: () => when(mockGetOrganizations( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenAnswer((_) async => orgs), act: (b) => b.add(const RefreshOrganizations()), expect: () => [ const OrganizationsLoading(), isA(), ], ); }); // ─── ResetOrganizationsState ────────────────────────────────────────────── group('ResetOrganizationsState', () { blocTest( 'emits OrganizationsInitial', build: buildBloc, seed: () => OrganizationsLoaded( organizations: [_makeOrg()], filteredOrganizations: [_makeOrg()], ), act: (b) => b.add(const ResetOrganizationsState()), expect: () => [const OrganizationsInitial()], ); }); // ─── DioException cancel is swallowed ───────────────────────────────────── group('DioException.cancel handling', () { blocTest( 'LoadOrganizations: cancel exception produces no error state', build: buildBloc, setUp: () { when(mockGetOrganizations( page: anyNamed('page'), size: anyNamed('size'), recherche: anyNamed('recherche'), )).thenThrow(DioException( requestOptions: RequestOptions(path: '/test'), type: DioExceptionType.cancel, )); }, act: (b) => b.add(const LoadOrganizations()), expect: () => [const OrganizationsLoading()], ); }); // ─── OrganizationsLoaded state helpers ──────────────────────────────────── group('OrganizationsLoaded state helpers', () { final activeOrg = _makeOrg(statut: StatutOrganization.active, nombreMembres: 5); final suspendedOrg = _makeOrg(id: 'org-2', statut: StatutOrganization.suspendue, nombreMembres: 3); final state = OrganizationsLoaded( organizations: [activeOrg, suspendedOrg], filteredOrganizations: [activeOrg], ); test('totalCount returns full organizations count', () { expect(state.totalCount, 2); }); test('filteredCount returns filtered count', () { expect(state.filteredCount, 1); }); test('quickStats calculates correctly', () { final stats = state.quickStats; expect(stats['total'], 2); expect(stats['actives'], 1); expect(stats['inactives'], 1); expect(stats['totalMembres'], 8); }); test('hasFilters is false when no filters applied', () { final clean = OrganizationsLoaded( organizations: [activeOrg], filteredOrganizations: [activeOrg], ); expect(clean.hasFilters, false); }); test('hasFilters is true when statusFilter set', () { final filtered = OrganizationsLoaded( organizations: [activeOrg], filteredOrganizations: [activeOrg], statusFilter: StatutOrganization.active, ); expect(filtered.hasFilters, true); }); test('copyWith with clearSearch resets currentSearch', () { final withSearch = OrganizationsLoaded( organizations: [activeOrg], filteredOrganizations: [activeOrg], currentSearch: 'test', ); final cleared = withSearch.copyWith(clearSearch: true); expect(cleared.currentSearch, isNull); }); }); }