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/dashboard/data/repositories/finance_repository.dart'; import 'package:unionflow_mobile_apps/features/dashboard/presentation/bloc/finance_bloc.dart'; import 'package:unionflow_mobile_apps/features/dashboard/presentation/bloc/finance_event.dart'; import 'package:unionflow_mobile_apps/features/dashboard/presentation/bloc/finance_state.dart'; @GenerateMocks([FinanceRepository]) import 'finance_bloc_test.mocks.dart'; // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const _summary = FinanceSummary( totalContributionsPaid: 150000, totalContributionsPending: 25000, epargneBalance: 300000, ); final _transactions = [ const FinanceTransaction( id: 'tx-1', title: 'Cotisation Janvier', date: '01/01/2026', amount: 5000, status: 'En attente', ), const FinanceTransaction( id: 'tx-2', title: 'Cotisation Février', date: '01/02/2026', amount: 5000, status: 'Payé', ), ]; // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { late MockFinanceRepository mockRepository; setUp(() { mockRepository = MockFinanceRepository(); }); FinanceBloc buildBloc() => FinanceBloc(mockRepository); // ─── initial state ─────────────────────────────────────────────────────── test('initial state is FinanceInitial', () { final bloc = buildBloc(); expect(bloc.state, isA()); bloc.close(); }); // ─── LoadFinanceRequested ──────────────────────────────────────────────── group('LoadFinanceRequested', () { blocTest( 'emits [FinanceLoading, FinanceLoaded] on success', build: () { when(mockRepository.getFinancialSummary()) .thenAnswer((_) async => _summary); when(mockRepository.getTransactions()) .thenAnswer((_) async => _transactions); return buildBloc(); }, act: (b) => b.add(LoadFinanceRequested()), expect: () => [ isA(), isA(), ], verify: (_) { verify(mockRepository.getFinancialSummary()).called(1); verify(mockRepository.getTransactions()).called(1); }, ); blocTest( 'FinanceLoaded contains correct summary and transactions', build: () { when(mockRepository.getFinancialSummary()) .thenAnswer((_) async => _summary); when(mockRepository.getTransactions()) .thenAnswer((_) async => _transactions); return buildBloc(); }, act: (b) => b.add(LoadFinanceRequested()), expect: () => [ isA(), FinanceLoaded(summary: _summary, transactions: _transactions), ], ); blocTest( 'emits [FinanceLoading, FinanceLoaded] with empty transactions list', build: () { when(mockRepository.getFinancialSummary()) .thenAnswer((_) async => _summary); when(mockRepository.getTransactions()) .thenAnswer((_) async => []); return buildBloc(); }, act: (b) => b.add(LoadFinanceRequested()), expect: () => [ isA(), const FinanceLoaded(summary: _summary, transactions: []), ], ); blocTest( 'emits [FinanceLoading, FinanceError] when getFinancialSummary throws', build: () { when(mockRepository.getFinancialSummary()) .thenThrow(Exception('network error')); when(mockRepository.getTransactions()) .thenAnswer((_) async => []); return buildBloc(); }, act: (b) => b.add(LoadFinanceRequested()), expect: () => [ isA(), isA(), ], ); blocTest( 'emits [FinanceLoading, FinanceError] when getTransactions throws', build: () { when(mockRepository.getFinancialSummary()) .thenAnswer((_) async => _summary); when(mockRepository.getTransactions()) .thenThrow(Exception('transactions error')); return buildBloc(); }, act: (b) => b.add(LoadFinanceRequested()), expect: () => [ isA(), isA(), ], ); blocTest( 'FinanceError message contains error information', build: () { when(mockRepository.getFinancialSummary()) .thenThrow(Exception('server down')); when(mockRepository.getTransactions()) .thenAnswer((_) async => []); return buildBloc(); }, act: (b) => b.add(LoadFinanceRequested()), expect: () => [ isA(), predicate( (s) => s is FinanceError && s.message.contains('Erreur chargement'), 'FinanceError with expected prefix', ), ], ); blocTest( 'does NOT emit error when DioException type is cancel', build: () { final dioException = DioException( requestOptions: RequestOptions(path: '/test'), type: DioExceptionType.cancel, ); when(mockRepository.getFinancialSummary()) .thenThrow(dioException); when(mockRepository.getTransactions()) .thenAnswer((_) async => []); return buildBloc(); }, act: (b) => b.add(LoadFinanceRequested()), // Only FinanceLoading emitted; the cancel exception is swallowed expect: () => [isA()], ); blocTest( 'handles multiple consecutive LoadFinanceRequested events', build: () { when(mockRepository.getFinancialSummary()) .thenAnswer((_) async => _summary); when(mockRepository.getTransactions()) .thenAnswer((_) async => _transactions); return buildBloc(); }, act: (b) async { b.add(LoadFinanceRequested()); // Wait for the first load to complete before firing the second await b.stream.firstWhere((s) => s is FinanceLoaded); b.add(LoadFinanceRequested()); }, expect: () => [ isA(), isA(), isA(), isA(), ], ); }); // ─── FinancePaymentInitiated ───────────────────────────────────────────── group('FinancePaymentInitiated', () { blocTest( 'does not change state when current state is FinanceInitial', build: buildBloc, act: (b) => b.add(const FinancePaymentInitiated('contrib-1')), expect: () => [], ); blocTest( 'does not change state when current state is FinanceLoaded (no-op for now)', build: () { // No repository calls needed — the handler is a no-op when loaded return buildBloc(); }, seed: () => FinanceLoaded(summary: _summary, transactions: _transactions), act: (b) => b.add(const FinancePaymentInitiated('contrib-42')), // The bloc intentionally keeps FinanceLoaded unchanged expect: () => [], ); blocTest( 'does not call any repository method', build: buildBloc, act: (b) => b.add(const FinancePaymentInitiated('contrib-1')), verify: (_) { verifyZeroInteractions(mockRepository); }, ); }); // ─── State equality ────────────────────────────────────────────────────── group('State equality', () { test('FinanceSummary equality works correctly', () { const s1 = FinanceSummary( totalContributionsPaid: 100, totalContributionsPending: 50, epargneBalance: 200, ); const s2 = FinanceSummary( totalContributionsPaid: 100, totalContributionsPending: 50, epargneBalance: 200, ); expect(s1, equals(s2)); }); test('FinanceTransaction equality works correctly', () { const tx1 = FinanceTransaction( id: 'tx-1', title: 'Title', date: '01/01/2026', amount: 5000, status: 'Payé', ); const tx2 = FinanceTransaction( id: 'tx-1', title: 'Title', date: '01/01/2026', amount: 5000, status: 'Payé', ); expect(tx1, equals(tx2)); }); test('FinanceLoaded equality works correctly', () { final l1 = FinanceLoaded(summary: _summary, transactions: _transactions); final l2 = FinanceLoaded(summary: _summary, transactions: _transactions); expect(l1, equals(l2)); }); test('FinanceError equality works correctly', () { const e1 = FinanceError('some error'); const e2 = FinanceError('some error'); expect(e1, equals(e2)); }); }); }