2026-05-17 18:40:03 +07:00

504 lines
18 KiB
Dart

// 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<NotifItem> 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<NotifItem> _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);
});
});
});
}