504 lines
18 KiB
Dart
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);
|
|
});
|
|
});
|
|
});
|
|
}
|