// test/widget/notification_screen_test.dart // ignore_for_file: prefer_const_constructors // // Widget tests untuk NotificationScreen — menampilkan notifikasi dari Guardian. // Jalankan: flutter test test/widget/notification_screen_test.dart // ignore_for_file: avoid_print import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; // --------------------------------------------------------------------------- // Stubs / Models // --------------------------------------------------------------------------- enum NotifType { text, voiceNote } class NotifItem { final int id; final NotifType type; final String content; final bool isRead; final DateTime createdAt; final int? voiceNoteDuration; const NotifItem({ required this.id, required this.type, required this.content, required this.isRead, required this.createdAt, this.voiceNoteDuration, }); } /// Stub NotificationScreen yang mereplikasi UI asli tanpa DI / API calls class _StubNotificationScreen extends StatefulWidget { final List notifications; final bool isLoading; final String? errorMessage; const _StubNotificationScreen({ this.notifications = const [], this.isLoading = false, this.errorMessage, }); @override State<_StubNotificationScreen> createState() => _StubNotificationScreenState(); } class _StubNotificationScreenState extends State<_StubNotificationScreen> { late List _items; bool _markingAll = false; @override void initState() { super.initState(); _items = List.from(widget.notifications); } void _markAllRead() { setState(() { _markingAll = true; _items = _items .map((e) => NotifItem( id: e.id, type: e.type, content: e.content, isRead: true, createdAt: e.createdAt, voiceNoteDuration: e.voiceNoteDuration, )) .toList(); _markingAll = false; }); } int get _unreadCount => _items.where((e) => !e.isRead).length; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Row( children: [ const Text('Notifikasi'), if (_unreadCount > 0) ...[ const SizedBox(width: 8), Container( key: const Key('unread_badge'), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(12), ), child: Text( '$_unreadCount', style: const TextStyle(color: Colors.white, fontSize: 12), ), ), ], ], ), actions: [ if (_items.any((e) => !e.isRead)) TextButton( key: const Key('mark_all_read_button'), onPressed: _markingAll ? null : _markAllRead, child: const Text('Tandai Semua Dibaca'), ), ], ), body: widget.isLoading ? const Center( child: CircularProgressIndicator(key: Key('loading_indicator'))) : widget.errorMessage != null ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, size: 48, color: Colors.red, key: Key('error_icon')), const SizedBox(height: 8), Text( key: const Key('error_message'), widget.errorMessage!, textAlign: TextAlign.center, ), const SizedBox(height: 16), ElevatedButton( key: const Key('retry_button'), onPressed: () {}, child: const Text('Coba Lagi'), ), ], ), ) : _items.isEmpty ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: const [ Icon(Icons.notifications_none, size: 64, color: Colors.grey, key: Key('empty_icon')), SizedBox(height: 8), Text( key: Key('empty_text'), 'Tidak ada notifikasi', style: TextStyle(color: Colors.grey), ), ], ), ) : RefreshIndicator( key: const Key('refresh_indicator'), onRefresh: () async {}, child: ListView.separated( key: const Key('notification_list'), padding: const EdgeInsets.all(12), itemCount: _items.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { final item = _items[index]; return _NotifTile( key: Key('notif_tile_${item.id}'), item: item, ); }, ), ), ); } } class _NotifTile extends StatelessWidget { final NotifItem item; const _NotifTile({super.key, required this.item}); @override Widget build(BuildContext context) { return ListTile( leading: CircleAvatar( backgroundColor: item.isRead ? Colors.grey.shade200 : Colors.blue.shade100, child: Icon( item.type == NotifType.voiceNote ? Icons.mic : Icons.message, key: Key('notif_icon_${item.id}'), color: item.isRead ? Colors.grey : Colors.blue, ), ), title: Text( key: Key('notif_content_${item.id}'), item.content, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: item.isRead ? FontWeight.normal : FontWeight.bold, ), ), subtitle: Row( children: [ Text( key: Key('notif_type_${item.id}'), item.type == NotifType.voiceNote ? 'Voice Note${item.voiceNoteDuration != null ? ' (${item.voiceNoteDuration}s)' : ''}' : 'Pesan Teks', style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), ], ), trailing: item.isRead ? null : Container( key: Key('unread_dot_${item.id}'), width: 8, height: 8, decoration: const BoxDecoration( color: Colors.blue, shape: BoxShape.circle, ), ), ); } } Widget makeTestable(Widget child) => MaterialApp(home: child); // Fixtures final _textNotif = NotifItem( id: 1, type: NotifType.text, content: 'Hati-hati di persimpangan depan', isRead: false, createdAt: DateTime(2025, 5, 10, 10, 30), ); final _voiceNotif = NotifItem( id: 2, type: NotifType.voiceNote, content: 'Voice note dari Guardian', isRead: false, createdAt: DateTime(2025, 5, 10, 11, 0), voiceNoteDuration: 12, ); final _readNotif = NotifItem( id: 3, type: NotifType.text, content: 'Sudah dibaca sebelumnya', isRead: true, createdAt: DateTime(2025, 5, 9, 9, 0), ); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- void main() { group('NotificationScreen Widget Tests', () { // ── Loading state ───────────────────────────────────────────────────── group('Loading state', () { testWidgets('menampilkan loading indicator saat isLoading=true', (tester) async { await tester.pumpWidget( makeTestable(const _StubNotificationScreen(isLoading: true)), ); expect(find.byKey(const Key('loading_indicator')), findsOneWidget); }); testWidgets('menyembunyikan list saat loading', (tester) async { await tester.pumpWidget( makeTestable(const _StubNotificationScreen(isLoading: true)), ); expect(find.byKey(const Key('notification_list')), findsNothing); }); }); // ── Error state ─────────────────────────────────────────────────────── group('Error state', () { testWidgets('menampilkan error message saat ada error', (tester) async { await tester.pumpWidget(makeTestable( const _StubNotificationScreen( errorMessage: 'Gagal memuat notifikasi'), )); expect(find.byKey(const Key('error_message')), findsOneWidget); expect(find.text('Gagal memuat notifikasi'), findsOneWidget); }); testWidgets('menampilkan icon error saat ada error', (tester) async { await tester.pumpWidget(makeTestable( const _StubNotificationScreen(errorMessage: 'Koneksi gagal'), )); expect(find.byKey(const Key('error_icon')), findsOneWidget); }); testWidgets('menampilkan tombol Retry saat error', (tester) async { await tester.pumpWidget(makeTestable( const _StubNotificationScreen(errorMessage: 'Timeout'), )); expect(find.byKey(const Key('retry_button')), findsOneWidget); }); }); // ── Empty state ─────────────────────────────────────────────────────── group('Empty state', () { testWidgets('menampilkan empty state saat tidak ada notifikasi', (tester) async { await tester.pumpWidget(makeTestable(const _StubNotificationScreen())); expect(find.byKey(const Key('empty_text')), findsOneWidget); expect(find.text('Tidak ada notifikasi'), findsOneWidget); }); testWidgets('menampilkan icon empty state', (tester) async { await tester.pumpWidget(makeTestable(const _StubNotificationScreen())); expect(find.byKey(const Key('empty_icon')), findsOneWidget); }); testWidgets('tidak menampilkan tombol mark all read saat list kosong', (tester) async { await tester.pumpWidget(makeTestable(const _StubNotificationScreen())); expect(find.byKey(const Key('mark_all_read_button')), findsNothing); }); }); // ── Daftar notifikasi ───────────────────────────────────────────────── group('Daftar notifikasi', () { testWidgets('menampilkan semua notifikasi dalam list', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen( notifications: [_textNotif, _voiceNotif, _readNotif]), )); expect(find.byKey(const Key('notification_list')), findsOneWidget); expect(find.byKey(const Key('notif_tile_1')), findsOneWidget); expect(find.byKey(const Key('notif_tile_2')), findsOneWidget); expect(find.byKey(const Key('notif_tile_3')), findsOneWidget); }); testWidgets('menampilkan konten notifikasi teks dengan benar', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen(notifications: [_textNotif]), )); expect(find.text('Hati-hati di persimpangan depan'), findsOneWidget); }); testWidgets('menampilkan badge unread count di AppBar', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen(notifications: [_textNotif, _voiceNotif]), )); expect(find.byKey(const Key('unread_badge')), findsOneWidget); expect(find.text('2'), findsOneWidget); }); testWidgets('tidak menampilkan unread badge saat semua sudah dibaca', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen(notifications: [_readNotif]), )); expect(find.byKey(const Key('unread_badge')), findsNothing); }); testWidgets('menampilkan unread dot untuk notifikasi belum dibaca', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen(notifications: [_textNotif]), )); expect(find.byKey(const Key('unread_dot_1')), findsOneWidget); }); testWidgets('tidak menampilkan unread dot untuk notif sudah dibaca', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen(notifications: [_readNotif]), )); expect(find.byKey(const Key('unread_dot_3')), findsNothing); }); testWidgets('menampilkan jenis voice note dengan durasi', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen(notifications: [_voiceNotif]), )); expect(find.textContaining('Voice Note'), findsOneWidget); expect(find.textContaining('12s'), findsOneWidget); }); testWidgets('menampilkan icon mic untuk voice note', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen(notifications: [_voiceNotif]), )); expect(find.byKey(const Key('notif_icon_2')), findsOneWidget); }); }); // ── Mark all read ───────────────────────────────────────────────────── group('Mark all read', () { testWidgets('menampilkan tombol mark all read saat ada unread', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen(notifications: [_textNotif, _voiceNotif]), )); expect(find.byKey(const Key('mark_all_read_button')), findsOneWidget); }); testWidgets('tap mark all read menghapus semua unread dot', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen(notifications: [_textNotif, _voiceNotif]), )); expect(find.byKey(const Key('unread_dot_1')), findsOneWidget); expect(find.byKey(const Key('unread_dot_2')), findsOneWidget); await tester.tap(find.byKey(const Key('mark_all_read_button'))); await tester.pump(); expect(find.byKey(const Key('unread_dot_1')), findsNothing); expect(find.byKey(const Key('unread_dot_2')), findsNothing); }); testWidgets('tap mark all read menghapus unread badge', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen(notifications: [_textNotif]), )); expect(find.byKey(const Key('unread_badge')), findsOneWidget); await tester.tap(find.byKey(const Key('mark_all_read_button'))); await tester.pump(); expect(find.byKey(const Key('unread_badge')), findsNothing); }); testWidgets('tombol mark all read hilang setelah semua dibaca', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen(notifications: [_textNotif]), )); await tester.tap(find.byKey(const Key('mark_all_read_button'))); await tester.pump(); expect(find.byKey(const Key('mark_all_read_button')), findsNothing); }); }); // ── RefreshIndicator ────────────────────────────────────────────────── group('Pull to refresh', () { testWidgets('terdapat RefreshIndicator di daftar notifikasi', (tester) async { await tester.pumpWidget(makeTestable( _StubNotificationScreen(notifications: [_textNotif]), )); expect(find.byKey(const Key('refresh_indicator')), findsOneWidget); }); }); // ── Layout responsif ────────────────────────────────────────────────── group('Responsif', () { testWidgets('tidak overflow pada layar kecil', (tester) async { tester.view.physicalSize = const Size(360, 640); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); await tester.pumpWidget(makeTestable( _StubNotificationScreen( notifications: [_textNotif, _voiceNotif, _readNotif]), )); expect(tester.takeException(), isNull); }); }); }); }