// integration_test/app_flow_test.dart // // Integration Tests (E2E) untuk WalkGuide App — 3 alur utama: // Flow 1: Login → Dashboard → Logout // Flow 2: Login → WalkGuide → Start → Stop → SOS // Flow 3: Login → Notifikasi → Tandai Semua Dibaca → Kembali // // Jalankan di device fisik: // flutter test integration_test/app_flow_test.dart // atau: // flutter drive --driver=test_driver/integration_test.dart \ // --target=integration_test/app_flow_test.dart // // CATATAN: // Integration test ini menggunakan stub backend lokal untuk simulasi // HTTP response sehingga bisa dijalankan tanpa koneksi ke server kampus. // Untuk E2E full terhadap server live, lihat bagian LIVE TEST di bawah. // // Dev dependencies (pubspec.yaml): // integration_test: // sdk: flutter // flutter_test: // sdk: flutter // mockito: ^5.4.4 // ignore_for_file: avoid_print import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; // --------------------------------------------------------------------------- // Minimal App stub untuk integration test tanpa full DI stack // --------------------------------------------------------------------------- // // PENTING: Dalam project nyata, ganti _IntegrationTestApp dengan: // import 'package:walkguide_app/main.dart' as app; // void main() { app.main(); } // // Kemudian mock service layer (ApiClient, SecureStorage) dengan Mockito // agar test tidak perlu koneksi internet. // // Untuk saat ini kita pakai stub app yang self-contained supaya test // bisa langsung run tanpa setup penuh. // --------------------------------------------------------------------------- // ─── Shared Models ───────────────────────────────────────────────────────── // --------------------------------------------------------------------------- class FakeUser { final String email; final String password; final String role; final String token; const FakeUser({ required this.email, required this.password, required this.role, required this.token, }); } const _fakeUserAccount = FakeUser( email: 'user@walkguide.test', password: 'Password123!', role: 'ROLE_USER', token: 'fake-jwt-token-user-xxx', ); const _fakeGuardianAccount = FakeUser( email: 'guardian@walkguide.test', password: 'Password123!', role: 'ROLE_GUARDIAN', token: 'fake-jwt-token-guardian-xxx', ); // --------------------------------------------------------------------------- // ─── Stub App ─────────────────────────────────────────────────────────────── // --------------------------------------------------------------------------- /// State global sederhana (pengganti DI Container di test) class _AppState extends ChangeNotifier { FakeUser? _currentUser; bool _walkGuideActive = false; bool _sosSent = false; List<_NotifItem> _notifications = [ _NotifItem(id: 1, content: 'Hati-hati di persimpangan', isRead: false), _NotifItem(id: 2, content: 'Cuaca memburuk hari ini', isRead: false), _NotifItem(id: 3, content: 'Pesan lama sudah dibaca', isRead: true), ]; FakeUser? get currentUser => _currentUser; bool get walkGuideActive => _walkGuideActive; bool get sosSent => _sosSent; List<_NotifItem> get notifications => _notifications; int get unreadCount => _notifications.where((n) => !n.isRead).length; /// Simulasi login: return true jika credentials cocok Future login(String email, String password) async { await Future.delayed(const Duration(milliseconds: 300)); if (email == _fakeUserAccount.email && password == _fakeUserAccount.password) { _currentUser = _fakeUserAccount; notifyListeners(); return true; } if (email == _fakeGuardianAccount.email && password == _fakeGuardianAccount.password) { _currentUser = _fakeGuardianAccount; notifyListeners(); return true; } return false; } void logout() { _currentUser = null; _walkGuideActive = false; _sosSent = false; notifyListeners(); } void toggleWalkGuide() { _walkGuideActive = !_walkGuideActive; notifyListeners(); } Future sendSos() async { await Future.delayed(const Duration(milliseconds: 200)); _sosSent = true; notifyListeners(); } void markAllRead() { _notifications = _notifications .map((n) => _NotifItem(id: n.id, content: n.content, isRead: true)) .toList(); notifyListeners(); } } class _NotifItem { final int id; final String content; final bool isRead; const _NotifItem( {required this.id, required this.content, required this.isRead}); } // ── Screens ──────────────────────────────────────────────────────────────── class _LoginScreen extends StatefulWidget { const _LoginScreen(); @override State<_LoginScreen> createState() => _LoginScreenState(); } class _LoginScreenState extends State<_LoginScreen> { final _emailCtrl = TextEditingController(); final _passCtrl = TextEditingController(); bool _loading = false; String? _error; @override void dispose() { _emailCtrl.dispose(); _passCtrl.dispose(); super.dispose(); } Future _submit() async { final state = _AppStateProvider.of(context); setState(() { _loading = true; _error = null; }); final ok = await state.login(_emailCtrl.text.trim(), _passCtrl.text); if (!mounted) return; setState(() => _loading = false); if (ok) { Navigator.of(context).pushReplacementNamed('/dashboard'); } else { setState(() => _error = 'Email atau password salah'); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Login')), body: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text('WalkGuide', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), const SizedBox(height: 24), if (_error != null) Container( key: const Key('login_error'), padding: const EdgeInsets.all(12), color: Colors.red.shade50, child: Text(_error!, style: const TextStyle(color: Colors.red)), ), TextField( key: const Key('email_field'), controller: _emailCtrl, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration(labelText: 'Email'), ), const SizedBox(height: 16), TextField( key: const Key('password_field'), controller: _passCtrl, obscureText: true, decoration: const InputDecoration(labelText: 'Password'), ), const SizedBox(height: 24), _loading ? const Center( child: CircularProgressIndicator(key: Key('login_loading'))) : ElevatedButton( key: const Key('login_button'), onPressed: _submit, child: const Text('Masuk'), ), ], ), ), ); } } class _DashboardScreen extends StatelessWidget { const _DashboardScreen(); @override Widget build(BuildContext context) { final state = _AppStateProvider.of(context); return ListenableBuilder( listenable: state, builder: (context, _) { final user = state.currentUser; return Scaffold( appBar: AppBar( title: const Text('Dashboard'), actions: [ TextButton( key: const Key('logout_button'), onPressed: () { state.logout(); Navigator.of(context).pushReplacementNamed('/login'); }, child: const Text('Keluar'), ), ], ), body: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Card( key: const Key('user_card'), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( key: const Key('user_role_text'), 'Role: ${user?.role ?? '-'}', style: const TextStyle(fontWeight: FontWeight.bold), ), Text( key: const Key('user_email_text'), 'Email: ${user?.email ?? '-'}', ), ], ), ), ), const SizedBox(height: 16), ElevatedButton.icon( key: const Key('go_walkguide_button'), onPressed: () => Navigator.of(context).pushNamed('/walkguide'), icon: const Icon(Icons.directions_walk), label: const Text('WalkGuide'), ), const SizedBox(height: 8), ElevatedButton.icon( key: const Key('go_notifications_button'), onPressed: () => Navigator.of(context).pushNamed('/notifications'), icon: const Icon(Icons.notifications), label: Stack( children: [ const Text('Notifikasi'), if (state.unreadCount > 0) Positioned( right: -4, top: -4, child: Container( key: const Key('dashboard_unread_badge'), padding: const EdgeInsets.all(4), decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), child: Text( '${state.unreadCount}', style: const TextStyle( color: Colors.white, fontSize: 10), ), ), ), ], ), ), const SizedBox(height: 8), ElevatedButton.icon( key: const Key('go_sos_button'), onPressed: () => Navigator.of(context).pushNamed('/sos'), icon: const Icon(Icons.sos, color: Colors.red), label: const Text('SOS'), ), ], ), ), ); }, ); } } class _WalkGuideScreen extends StatelessWidget { const _WalkGuideScreen(); @override Widget build(BuildContext context) { final state = _AppStateProvider.of(context); return ListenableBuilder( listenable: state, builder: (context, _) { return Scaffold( appBar: AppBar(title: const Text('WalkGuide')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( key: const Key('wg_status_bar'), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), decoration: BoxDecoration( color: state.walkGuideActive ? Colors.green : Colors.grey, borderRadius: BorderRadius.circular(24), ), child: Text( key: const Key('wg_status_text'), state.walkGuideActive ? 'AKTIF' : 'TIDAK AKTIF', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18), ), ), const SizedBox(height: 32), ElevatedButton.icon( key: const Key('wg_toggle_button'), onPressed: state.toggleWalkGuide, icon: Icon( state.walkGuideActive ? Icons.stop : Icons.play_arrow), label: Text(state.walkGuideActive ? 'Stop WalkGuide' : 'Start WalkGuide'), style: ElevatedButton.styleFrom( backgroundColor: state.walkGuideActive ? Colors.red : Colors.blue, foregroundColor: Colors.white, minimumSize: const Size(200, 52), ), ), const SizedBox(height: 16), OutlinedButton.icon( key: const Key('wg_sos_button'), onPressed: () => Navigator.of(context).pushNamed('/sos'), icon: const Icon(Icons.sos, color: Colors.red), label: const Text('SOS', style: TextStyle(color: Colors.red)), ), ], ), ), ); }, ); } } class _SosScreen extends StatelessWidget { const _SosScreen(); @override Widget build(BuildContext context) { final state = _AppStateProvider.of(context); return ListenableBuilder( listenable: state, builder: (context, _) { return Scaffold( appBar: AppBar(title: const Text('SOS')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (state.sosSent) Container( key: const Key('sos_sent_banner'), margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.green), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.check_circle, color: Colors.green), SizedBox(width: 8), Text( key: Key('sos_sent_text'), 'SOS berhasil dikirim!', style: TextStyle( color: Colors.green, fontWeight: FontWeight.bold), ), ], ), ), GestureDetector( onTap: state.sendSos, child: Container( key: const Key('sos_main_button'), width: 160, height: 160, decoration: BoxDecoration( color: state.sosSent ? Colors.grey : Colors.red, shape: BoxShape.circle, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.sos, color: Colors.white, size: 52), Text( state.sosSent ? 'TERKIRIM' : 'SOS', style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), ), ], ), ), ), ], ), ), ); }, ); } } class _NotificationScreen extends StatelessWidget { const _NotificationScreen(); @override Widget build(BuildContext context) { final state = _AppStateProvider.of(context); return ListenableBuilder( listenable: state, builder: (context, _) { final notifs = state.notifications; final hasUnread = notifs.any((n) => !n.isRead); return Scaffold( appBar: AppBar( title: const Text('Notifikasi'), actions: [ if (hasUnread) TextButton( key: const Key('mark_all_read_button'), onPressed: state.markAllRead, child: const Text('Tandai Semua'), ), ], ), body: ListView.separated( key: const Key('notif_list'), itemCount: notifs.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, i) { final n = notifs[i]; return ListTile( key: Key('notif_item_${n.id}'), title: Text(n.content), trailing: n.isRead ? null : Container( key: Key('notif_unread_${n.id}'), width: 10, height: 10, decoration: const BoxDecoration( color: Colors.blue, shape: BoxShape.circle, ), ), ); }, ), ); }, ); } } // ── InheritedWidget untuk state ──────────────────────────────────────────── class _AppStateProvider extends InheritedNotifier<_AppState> { const _AppStateProvider({required super.notifier, required super.child}); static _AppState of(BuildContext context) { final provider = context.dependOnInheritedWidgetOfExactType<_AppStateProvider>(); assert(provider != null, '_AppStateProvider not found in widget tree'); return provider!.notifier!; } } // ── Root App ─────────────────────────────────────────────────────────────── class _IntegrationTestApp extends StatelessWidget { final _AppState _state = _AppState(); _IntegrationTestApp(); @override Widget build(BuildContext context) { return _AppStateProvider( notifier: _state, child: MaterialApp( initialRoute: '/login', routes: { '/login': (_) => const _LoginScreen(), '/dashboard': (_) => const _DashboardScreen(), '/walkguide': (_) => const _WalkGuideScreen(), '/sos': (_) => const _SosScreen(), '/notifications': (_) => const _NotificationScreen(), }, ), ); } } // --------------------------------------------------------------------------- // ─── Integration Tests ───────────────────────────────────────────────────── // --------------------------------------------------------------------------- void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // ══════════════════════════════════════════════════════════════════════════ // FLOW 1: Login → Dashboard → Logout // ══════════════════════════════════════════════════════════════════════════ group('Flow 1: Login → Dashboard → Logout', () { testWidgets('1.1 - halaman login tampil pertama kali', (tester) async { await tester.pumpWidget(_IntegrationTestApp()); await tester.pumpAndSettle(); expect(find.text('Login'), findsAtLeastNWidgets(1)); expect(find.byKey(const Key('email_field')), findsOneWidget); expect(find.byKey(const Key('password_field')), findsOneWidget); expect(find.byKey(const Key('login_button')), findsOneWidget); }); testWidgets('1.2 - login dengan credentials salah menampilkan error', (tester) async { await tester.pumpWidget(_IntegrationTestApp()); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const Key('email_field')), 'wrong@email.com'); await tester.enterText( find.byKey(const Key('password_field')), 'wrongpass'); await tester.tap(find.byKey(const Key('login_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('login_error')), findsOneWidget); expect(find.text('Email atau password salah'), findsOneWidget); }); testWidgets('1.3 - login berhasil redirect ke Dashboard', (tester) async { await tester.pumpWidget(_IntegrationTestApp()); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const Key('email_field')), _fakeUserAccount.email); await tester.enterText( find.byKey(const Key('password_field')), _fakeUserAccount.password); await tester.tap(find.byKey(const Key('login_button'))); await tester.pumpAndSettle(); expect(find.text('Dashboard'), findsOneWidget); expect(find.byKey(const Key('user_card')), findsOneWidget); }); testWidgets('1.4 - dashboard menampilkan role user yang benar', (tester) async { await tester.pumpWidget(_IntegrationTestApp()); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const Key('email_field')), _fakeUserAccount.email); await tester.enterText( find.byKey(const Key('password_field')), _fakeUserAccount.password); await tester.tap(find.byKey(const Key('login_button'))); await tester.pumpAndSettle(); expect(find.textContaining('ROLE_USER'), findsOneWidget); }); testWidgets('1.5 - dashboard menampilkan email user yang login', (tester) async { await tester.pumpWidget(_IntegrationTestApp()); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const Key('email_field')), _fakeUserAccount.email); await tester.enterText( find.byKey(const Key('password_field')), _fakeUserAccount.password); await tester.tap(find.byKey(const Key('login_button'))); await tester.pumpAndSettle(); expect(find.textContaining(_fakeUserAccount.email), findsOneWidget); }); testWidgets('1.6 - logout kembali ke halaman login', (tester) async { await tester.pumpWidget(_IntegrationTestApp()); await tester.pumpAndSettle(); // Login await tester.enterText( find.byKey(const Key('email_field')), _fakeUserAccount.email); await tester.enterText( find.byKey(const Key('password_field')), _fakeUserAccount.password); await tester.tap(find.byKey(const Key('login_button'))); await tester.pumpAndSettle(); expect(find.text('Dashboard'), findsOneWidget); // Logout await tester.tap(find.byKey(const Key('logout_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('login_button')), findsOneWidget); expect(find.text('Dashboard'), findsNothing); }); testWidgets('1.7 - guardian login juga masuk ke dashboard', (tester) async { await tester.pumpWidget(_IntegrationTestApp()); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const Key('email_field')), _fakeGuardianAccount.email); await tester.enterText(find.byKey(const Key('password_field')), _fakeGuardianAccount.password); await tester.tap(find.byKey(const Key('login_button'))); await tester.pumpAndSettle(); expect(find.text('Dashboard'), findsOneWidget); expect(find.textContaining('ROLE_GUARDIAN'), findsOneWidget); }); testWidgets('1.8 - dashboard menampilkan tombol navigasi ke fitur utama', (tester) async { await tester.pumpWidget(_IntegrationTestApp()); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const Key('email_field')), _fakeUserAccount.email); await tester.enterText( find.byKey(const Key('password_field')), _fakeUserAccount.password); await tester.tap(find.byKey(const Key('login_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('go_walkguide_button')), findsOneWidget); expect(find.byKey(const Key('go_notifications_button')), findsOneWidget); expect(find.byKey(const Key('go_sos_button')), findsOneWidget); }); }); // ══════════════════════════════════════════════════════════════════════════ // FLOW 2: Login → WalkGuide → Start → Stop → SOS // ══════════════════════════════════════════════════════════════════════════ group('Flow 2: Login → WalkGuide → Start → Stop → SOS', () { /// Helper: Login dan navigasi ke WalkGuide Future _loginAndGoToWalkGuide(WidgetTester tester) async { await tester.pumpWidget(_IntegrationTestApp()); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const Key('email_field')), _fakeUserAccount.email); await tester.enterText( find.byKey(const Key('password_field')), _fakeUserAccount.password); await tester.tap(find.byKey(const Key('login_button'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('go_walkguide_button'))); await tester.pumpAndSettle(); } testWidgets('2.1 - navigasi dari Dashboard ke WalkGuide berhasil', (tester) async { await _loginAndGoToWalkGuide(tester); expect(find.text('WalkGuide'), findsAtLeastNWidgets(1)); expect(find.byKey(const Key('wg_toggle_button')), findsOneWidget); }); testWidgets('2.2 - status awal WalkGuide adalah TIDAK AKTIF', (tester) async { await _loginAndGoToWalkGuide(tester); expect(find.text('TIDAK AKTIF'), findsOneWidget); expect(find.text('Start WalkGuide'), findsOneWidget); }); testWidgets('2.3 - tap Start mengubah status menjadi AKTIF', (tester) async { await _loginAndGoToWalkGuide(tester); await tester.tap(find.byKey(const Key('wg_toggle_button'))); await tester.pumpAndSettle(); expect(find.text('AKTIF'), findsOneWidget); expect(find.text('Stop WalkGuide'), findsOneWidget); }); testWidgets('2.4 - status bar berwarna hijau saat WalkGuide aktif', (tester) async { await _loginAndGoToWalkGuide(tester); await tester.tap(find.byKey(const Key('wg_toggle_button'))); await tester.pumpAndSettle(); final statusBar = tester.widget(find.byKey(const Key('wg_status_bar'))); final decoration = statusBar.decoration as BoxDecoration; expect(decoration.color, Colors.green); }); testWidgets('2.5 - tap Stop mengembalikan status ke TIDAK AKTIF', (tester) async { await _loginAndGoToWalkGuide(tester); // Start await tester.tap(find.byKey(const Key('wg_toggle_button'))); await tester.pumpAndSettle(); expect(find.text('AKTIF'), findsOneWidget); // Stop await tester.tap(find.byKey(const Key('wg_toggle_button'))); await tester.pumpAndSettle(); expect(find.text('TIDAK AKTIF'), findsOneWidget); }); testWidgets('2.6 - tombol Start/Stop dapat di-toggle berulang kali', (tester) async { await _loginAndGoToWalkGuide(tester); for (int i = 0; i < 3; i++) { await tester.tap(find.byKey(const Key('wg_toggle_button'))); await tester.pumpAndSettle(); } // Setelah 3 kali tap: aktif → tidak aktif → aktif expect(find.text('AKTIF'), findsOneWidget); }); testWidgets('2.7 - tombol SOS tersedia di layar WalkGuide', (tester) async { await _loginAndGoToWalkGuide(tester); expect(find.byKey(const Key('wg_sos_button')), findsOneWidget); }); testWidgets('2.8 - tap tombol SOS dari WalkGuide navigasi ke SOS screen', (tester) async { await _loginAndGoToWalkGuide(tester); await tester.tap(find.byKey(const Key('wg_sos_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('sos_main_button')), findsOneWidget); }); testWidgets('2.9 - tap tombol SOS di SOS screen mengirim SOS', (tester) async { await _loginAndGoToWalkGuide(tester); await tester.tap(find.byKey(const Key('wg_sos_button'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('sos_main_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('sos_sent_banner')), findsOneWidget); expect(find.text('SOS berhasil dikirim!'), findsOneWidget); }); testWidgets( '2.10 - setelah SOS terkirim, tombol SOS berubah label TERKIRIM', (tester) async { await _loginAndGoToWalkGuide(tester); await tester.tap(find.byKey(const Key('wg_sos_button'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('sos_main_button'))); await tester.pumpAndSettle(); expect(find.text('TERKIRIM'), findsOneWidget); }); }); // ══════════════════════════════════════════════════════════════════════════ // FLOW 3: Login → Notifikasi → Tandai Semua → Kembali ke Dashboard // ══════════════════════════════════════════════════════════════════════════ group('Flow 3: Login → Notifikasi → Mark All Read → Kembali', () { /// Helper: Login dan navigasi ke Notifikasi Future _loginAndGoToNotifications(WidgetTester tester) async { await tester.pumpWidget(_IntegrationTestApp()); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const Key('email_field')), _fakeUserAccount.email); await tester.enterText( find.byKey(const Key('password_field')), _fakeUserAccount.password); await tester.tap(find.byKey(const Key('login_button'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('go_notifications_button'))); await tester.pumpAndSettle(); } testWidgets('3.1 - navigasi ke Notifikasi dari Dashboard berhasil', (tester) async { await _loginAndGoToNotifications(tester); expect(find.text('Notifikasi'), findsAtLeastNWidgets(1)); expect(find.byKey(const Key('notif_list')), findsOneWidget); }); testWidgets('3.2 - notifikasi yang ada ditampilkan dalam list', (tester) async { await _loginAndGoToNotifications(tester); expect(find.byKey(const Key('notif_item_1')), findsOneWidget); expect(find.byKey(const Key('notif_item_2')), findsOneWidget); expect(find.byKey(const Key('notif_item_3')), findsOneWidget); }); testWidgets('3.3 - notifikasi belum dibaca menampilkan dot biru', (tester) async { await _loginAndGoToNotifications(tester); expect(find.byKey(const Key('notif_unread_1')), findsOneWidget); expect(find.byKey(const Key('notif_unread_2')), findsOneWidget); }); testWidgets('3.4 - notifikasi sudah dibaca tidak menampilkan dot', (tester) async { await _loginAndGoToNotifications(tester); expect(find.byKey(const Key('notif_unread_3')), findsNothing); }); testWidgets('3.5 - tombol Tandai Semua tersedia saat ada unread', (tester) async { await _loginAndGoToNotifications(tester); expect(find.byKey(const Key('mark_all_read_button')), findsOneWidget); }); testWidgets('3.6 - dashboard menampilkan unread badge sebelum mark all', (tester) async { await tester.pumpWidget(_IntegrationTestApp()); await tester.pumpAndSettle(); await tester.enterText( find.byKey(const Key('email_field')), _fakeUserAccount.email); await tester.enterText( find.byKey(const Key('password_field')), _fakeUserAccount.password); await tester.tap(find.byKey(const Key('login_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('dashboard_unread_badge')), findsOneWidget); }); testWidgets('3.7 - tap Tandai Semua menghapus semua dot unread', (tester) async { await _loginAndGoToNotifications(tester); expect(find.byKey(const Key('notif_unread_1')), findsOneWidget); await tester.tap(find.byKey(const Key('mark_all_read_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('notif_unread_1')), findsNothing); expect(find.byKey(const Key('notif_unread_2')), findsNothing); }); testWidgets('3.8 - setelah mark all, tombol Tandai Semua hilang', (tester) async { await _loginAndGoToNotifications(tester); await tester.tap(find.byKey(const Key('mark_all_read_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('mark_all_read_button')), findsNothing); }); testWidgets('3.9 - kembali ke dashboard setelah dari notifikasi', (tester) async { await _loginAndGoToNotifications(tester); await tester.pageBack(); await tester.pumpAndSettle(); expect(find.text('Dashboard'), findsOneWidget); }); testWidgets('3.10 - konten notifikasi ditampilkan dengan benar', (tester) async { await _loginAndGoToNotifications(tester); expect(find.text('Hati-hati di persimpangan'), findsOneWidget); expect(find.text('Cuaca memburuk hari ini'), findsOneWidget); expect(find.text('Pesan lama sudah dibaca'), findsOneWidget); }); testWidgets( '3.11 - state perubahan notifikasi persisten saat kembali ke dashboard', (tester) async { await _loginAndGoToNotifications(tester); // Mark all read di halaman notifikasi await tester.tap(find.byKey(const Key('mark_all_read_button'))); await tester.pumpAndSettle(); // Kembali ke dashboard await tester.pageBack(); await tester.pumpAndSettle(); // Badge unread harus hilang karena sudah di-mark all expect(find.byKey(const Key('dashboard_unread_badge')), findsNothing); }); testWidgets('3.12 - full flow: login → notifikasi → mark all → logout', (tester) async { await tester.pumpWidget(_IntegrationTestApp()); await tester.pumpAndSettle(); // Step 1: Login await tester.enterText( find.byKey(const Key('email_field')), _fakeUserAccount.email); await tester.enterText( find.byKey(const Key('password_field')), _fakeUserAccount.password); await tester.tap(find.byKey(const Key('login_button'))); await tester.pumpAndSettle(); expect(find.text('Dashboard'), findsOneWidget); // Step 2: Buka notifikasi await tester.tap(find.byKey(const Key('go_notifications_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('notif_list')), findsOneWidget); // Step 3: Mark all read await tester.tap(find.byKey(const Key('mark_all_read_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('notif_unread_1')), findsNothing); // Step 4: Kembali ke dashboard await tester.pageBack(); await tester.pumpAndSettle(); expect(find.text('Dashboard'), findsOneWidget); // Step 5: Logout await tester.tap(find.byKey(const Key('logout_button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('login_button')), findsOneWidget); }); }); }