import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; import 'package:kmobile/data/models/transaction.dart'; import 'package:kmobile/data/models/user.dart'; import 'package:kmobile/data/repositories/transaction_repository.dart'; import 'package:kmobile/features/accounts/screens/account_statement_screen.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:kmobile/l10n/app_localizations.dart'; // ─── Mock TransactionRepository ────────────────────────────────────────────── class MockTransactionRepository implements TransactionRepository { List mockTransactions = []; bool shouldThrow = false; String? lastAccountNo; DateTime? lastFromDate; DateTime? lastToDate; int callCount = 0; @override Future> fetchTransactions( String accountNo, { DateTime? fromDate, DateTime? toDate, }) async { callCount++; lastAccountNo = accountNo; lastFromDate = fromDate; lastToDate = toDate; if (shouldThrow) { throw Exception('Network error'); } return mockTransactions; } } // ─── Test Helpers ──────────────────────────────────────────────────────────── final getIt = GetIt.instance; User _createTestUser({ String accountNo = '1234567890', String name = 'Test User', String availableBalance = '50000', String branchId = 'BR001', String cifNumber = 'CIF123', String address = '123 Main Street', }) { return User( accountNo: accountNo, accountType: 'SB', branchId: branchId, currency: 'INR', availableBalance: availableBalance, currentBalance: availableBalance, name: name, mobileNo: '9876543210', address: address, picode: '176215', cifNumber: cifNumber, ); } Transaction _createTestTransaction({ String id = '1', String name = 'UPI Payment', String date = '01-01-2026', String amount = '1000', String type = 'DR', String balance = '49000', String balanceType = 'CR', }) { return Transaction( id: id, name: name, date: date, amount: amount, type: type, balance: balance, balanceType: balanceType, ); } /// Wraps the widget with MaterialApp + localizations for testing Widget _buildTestApp({ required List users, int selectedIndex = 0, }) { return MaterialApp( localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [Locale('en')], locale: const Locale('en'), home: AccountStatementScreen( users: users, selectedIndex: selectedIndex, ), ); } // ─── Tests ─────────────────────────────────────────────────────────────────── void main() { late MockTransactionRepository mockRepo; late User testUser; late User testUser2; setUp(() { // Reset GetIt before each test getIt.reset(); mockRepo = MockTransactionRepository(); getIt.registerSingleton(mockRepo); testUser = _createTestUser(); testUser2 = _createTestUser( accountNo: '9876543210', name: 'Second User', availableBalance: '75000', ); }); tearDown(() { getIt.reset(); }); // ═══════════════════════════════════════════════════════════════════════════ // GROUP 1: Screen Rendering // ═══════════════════════════════════════════════════════════════════════════ group('Screen Rendering', () { testWidgets('renders AppBar with "Account Statement" title', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); expect(find.text('Account Statement'), findsOneWidget); }); testWidgets('renders account number card with label', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); expect(find.text('Account Number'), findsOneWidget); }); testWidgets('renders available balance card', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); // The balance text shows ' ₹ 50000' expect(find.textContaining('50000'), findsOneWidget); }); testWidgets('renders From Date and To Date filter boxes', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); expect(find.text('From Date'), findsOneWidget); expect(find.text('To Date'), findsOneWidget); }); testWidgets('renders Search button', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); expect(find.text('Search'), findsOneWidget); }); testWidgets('renders floating action button for PDF download', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); expect(find.byType(FloatingActionButton), findsOneWidget); expect(find.byIcon(Icons.download), findsOneWidget); }); testWidgets('renders watermark logo with low opacity', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); // There should be an Opacity widget wrapping the logo final opacityFinder = find.byWidgetPredicate( (widget) => widget is Opacity && widget.opacity == 0.07, ); expect(opacityFinder, findsOneWidget); }); }); // ═══════════════════════════════════════════════════════════════════════════ // GROUP 2: Account Dropdown // ═══════════════════════════════════════════════════════════════════════════ group('Account Dropdown', () { testWidgets('displays selected user account number in dropdown', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser, testUser2])); await tester.pumpAndSettle(); expect(find.text('1234567890'), findsOneWidget); }); testWidgets('dropdown shows all user accounts when tapped', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser, testUser2])); await tester.pumpAndSettle(); // Tap the dropdown await tester.tap(find.byType(DropdownButton)); await tester.pumpAndSettle(); // Both accounts should be visible in the dropdown menu expect(find.text('1234567890'), findsWidgets); expect(find.text('9876543210'), findsWidgets); }); testWidgets('selecting another account triggers transaction reload', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser, testUser2])); await tester.pumpAndSettle(); final initialCallCount = mockRepo.callCount; // Open dropdown and select second user await tester.tap(find.byType(DropdownButton)); await tester.pumpAndSettle(); // Tap the second account (appears in the overlay) await tester.tap(find.text('9876543210').last); await tester.pumpAndSettle(); // Verify repository was called again with the new account expect(mockRepo.callCount, greaterThan(initialCallCount)); expect(mockRepo.lastAccountNo, '9876543210'); }); testWidgets('balance updates when user is switched', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser, testUser2])); await tester.pumpAndSettle(); expect(find.textContaining('50000'), findsOneWidget); // Switch to second user await tester.tap(find.byType(DropdownButton)); await tester.pumpAndSettle(); await tester.tap(find.text('9876543210').last); await tester.pumpAndSettle(); // Balance should now show second user's balance expect(find.textContaining('75000'), findsOneWidget); }); }); // ═══════════════════════════════════════════════════════════════════════════ // GROUP 3: Loading State // ═══════════════════════════════════════════════════════════════════════════ group('Loading State', () { testWidgets('shows shimmer loading effect initially', (tester) async { // Make the repo slow so loading state is visible mockRepo = MockTransactionRepository(); getIt.allowReassignment = true; getIt.registerSingleton(mockRepo); await tester.pumpWidget(_buildTestApp(users: [testUser])); // Don't call pumpAndSettle — we want to see the loading state await tester.pump(); // Check for shimmer placeholders (CircleAvatar is used in loading tiles) expect(find.byType(CircleAvatar), findsWidgets); }); }); // ═══════════════════════════════════════════════════════════════════════════ // GROUP 4: Empty State // ═══════════════════════════════════════════════════════════════════════════ group('Empty State', () { testWidgets('shows "No Transactions" when list is empty', (tester) async { mockRepo.mockTransactions = []; await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); expect(find.text('No Transactions'), findsOneWidget); }); }); // ═══════════════════════════════════════════════════════════════════════════ // GROUP 5: Transaction List // ═══════════════════════════════════════════════════════════════════════════ group('Transaction List', () { testWidgets('displays transaction list when data is loaded', (tester) async { mockRepo.mockTransactions = [ _createTestTransaction( name: 'UPI Payment', date: '01-01-2026', amount: '1000', type: 'DR', balance: '49000', ), _createTestTransaction( id: '2', name: 'Salary Credit', date: '02-01-2026', amount: '50000', type: 'CR', balance: '99000', ), ]; await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); expect(find.text('01-01-2026'), findsOneWidget); expect(find.text('02-01-2026'), findsOneWidget); expect(find.text('UPI Payment'), findsOneWidget); expect(find.text('Salary Credit'), findsOneWidget); }); testWidgets('displays amounts with rupee symbol', (tester) async { mockRepo.mockTransactions = [ _createTestTransaction(amount: '1500', balance: '48500'), ]; await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); expect(find.text('₹1500'), findsOneWidget); expect(find.text('Bal: ₹48500'), findsOneWidget); }); testWidgets('truncates long transaction names to 22 characters', (tester) async { mockRepo.mockTransactions = [ _createTestTransaction( name: 'This is a very long transaction name exceeding limit', ), ]; await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); // Only first 22 characters should be displayed expect(find.text('This is a very long tr'), findsOneWidget); }); testWidgets('shows "Last 10 Transactions" label when no date filter set', (tester) async { mockRepo.mockTransactions = [ _createTestTransaction(), ]; await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); expect(find.text('Last 10 Transactions'), findsOneWidget); }); testWidgets('credit transactions show green icon', (tester) async { mockRepo.mockTransactions = [ _createTestTransaction(type: 'CR'), ]; await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); // Find the Icon for credit transactions (call_received) final iconFinder = find.byWidgetPredicate( (widget) => widget is Icon && widget.color == const Color(0xFF10BB10), ); expect(iconFinder, findsOneWidget); }); }); // ═══════════════════════════════════════════════════════════════════════════ // GROUP 6: Error State // ═══════════════════════════════════════════════════════════════════════════ group('Error State', () { testWidgets('shows snackbar on transaction load failure', (tester) async { mockRepo.shouldThrow = true; await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsOneWidget); expect( find.textContaining('Failed to load transactions'), findsOneWidget); }); }); // ═══════════════════════════════════════════════════════════════════════════ // GROUP 7: Date Filters // ═══════════════════════════════════════════════════════════════════════════ group('Date Filters', () { testWidgets('tapping "From Date" opens a date picker', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); await tester.tap(find.text('From Date')); await tester.pumpAndSettle(); // The DatePicker dialog should be open expect(find.byType(DatePickerDialog), findsOneWidget); }); testWidgets('tapping "To Date" without From Date shows error snackbar', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); await tester.tap(find.text('To Date')); await tester.pumpAndSettle(); // Should show snackbar asking to select From Date first expect(find.byType(SnackBar), findsOneWidget); }); }); // ═══════════════════════════════════════════════════════════════════════════ // GROUP 8: Search Button // ═══════════════════════════════════════════════════════════════════════════ group('Search Button', () { testWidgets('pressing Search reloads transactions', (tester) async { await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); final callCountBefore = mockRepo.callCount; await tester.tap(find.text('Search')); await tester.pumpAndSettle(); expect(mockRepo.callCount, greaterThan(callCountBefore)); }); }); // ═══════════════════════════════════════════════════════════════════════════ // GROUP 9: PDF Export // ═══════════════════════════════════════════════════════════════════════════ group('PDF Export', () { testWidgets('pressing download FAB with no transactions shows snackbar', (tester) async { mockRepo.mockTransactions = []; await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); await tester.tap(find.byType(FloatingActionButton)); await tester.pumpAndSettle(); expect(find.text('No transactions to export.'), findsOneWidget); }); }); // ═══════════════════════════════════════════════════════════════════════════ // GROUP 10: Selected Index // ═══════════════════════════════════════════════════════════════════════════ group('Selected Index', () { testWidgets('uses selectedIndex to pick initial user', (tester) async { await tester.pumpWidget( _buildTestApp(users: [testUser, testUser2], selectedIndex: 1), ); await tester.pumpAndSettle(); // Second user's balance should be shown expect(find.textContaining('75000'), findsOneWidget); // Repository should have been called with second user's account expect(mockRepo.lastAccountNo, '9876543210'); }); }); // ═══════════════════════════════════════════════════════════════════════════ // GROUP 11: Navigation // ═══════════════════════════════════════════════════════════════════════════ group('Navigation', () { testWidgets('tapping a transaction navigates to details screen', (tester) async { mockRepo.mockTransactions = [ _createTestTransaction(name: 'Test Payment'), ]; await tester.pumpWidget(_buildTestApp(users: [testUser])); await tester.pumpAndSettle(); // Tap on the transaction ListTile await tester.tap(find.text('Test Payment')); await tester.pumpAndSettle(); // We should have navigated away from the statement screen // The TransactionDetailsScreen should now be visible expect(find.byType(AccountStatementScreen), findsNothing); }); }); }