import 'package:flutter_test/flutter_test.dart'; import 'package:meeyao_qianwen/features/notifications/data/models/notification_item.dart'; import 'package:meeyao_qianwen/features/notifications/data/models/notification_list_result.dart'; import 'package:meeyao_qianwen/features/notifications/data/models/notification_payload.dart'; import 'package:meeyao_qianwen/features/notifications/data/repositories/notification_repository.dart'; import 'package:meeyao_qianwen/features/notifications/presentation/bloc/notification_bloc.dart'; class _FakeNotificationRepository implements NotificationRepository { final List items = []; int unreadCount = 0; int markAllReadCallCount = 0; bool failMarkRead = false; bool failMarkAllRead = false; @override Future listNotifications({ int limit = 20, String? cursor, String locale = 'zh', }) async { return NotificationListResult( items: items, hasMore: false, nextCursor: null, ); } @override Future getUnreadCount() async => unreadCount; @override Future markRead({ required String notificationId, String locale = 'zh', }) async { if (failMarkRead) { throw Exception('Mark read failed'); } final idx = items.indexWhere((i) => i.id == notificationId); if (idx == -1) { throw Exception('Not found'); } items[idx] = items[idx].copyWith(isRead: true); unreadCount = unreadCount > 0 ? unreadCount - 1 : 0; return items[idx]; } @override Future markAllRead() async { if (failMarkAllRead) { throw Exception('Mark all read failed'); } markAllReadCallCount++; final count = unreadCount; for (int i = 0; i < items.length; i++) { items[i] = items[i].copyWith(isRead: true); } unreadCount = 0; return count; } } NotificationItem makeItem({ String id = 'item-1', String notificationId = 'notif-1', bool isRead = false, }) { return NotificationItem( id: id, notificationId: notificationId, type: 'system', title: 'Test', body: 'Body', payload: const NotificationPayloadNone(), isRead: isRead, createdAt: DateTime(2026, 4, 10), ); } void main() { group('NotificationBloc', () { late _FakeNotificationRepository fakeRepo; late NotificationBloc bloc; setUp(() { fakeRepo = _FakeNotificationRepository(); bloc = NotificationBloc(repository: fakeRepo); }); tearDown(() { bloc.dispose(); }); test('initial state has zero unreadCount', () { expect(bloc.state.unreadCount, 0); expect(bloc.state.items, isEmpty); }); test('RefreshUnreadCount updates unreadCount', () async { fakeRepo.unreadCount = 5; await bloc.handleEvent(RefreshUnreadCount()); expect(bloc.state.unreadCount, 5); }); test('MarkNotificationRead marks item as read', () async { fakeRepo.items.add(makeItem(id: 'n1', isRead: false)); await bloc.handleEvent(LoadNotifications()); await bloc.handleEvent(MarkNotificationRead(notificationId: 'n1')); expect(bloc.state.items.first.isRead, true); expect(bloc.state.unreadCount, 0); }); test( 'MarkNotificationRead does not update state when request fails', () async { fakeRepo.items.add(makeItem(id: 'n1', isRead: false)); fakeRepo.unreadCount = 1; fakeRepo.failMarkRead = true; await bloc.handleEvent(LoadNotifications()); await bloc.handleEvent(RefreshUnreadCount()); await bloc.handleEvent(MarkNotificationRead(notificationId: 'n1')); expect(bloc.state.items.first.isRead, false); expect(bloc.state.unreadCount, 1); }, ); test('MarkAllNotificationsRead marks all as read', () async { fakeRepo.items.addAll([ makeItem(id: 'n1', isRead: false), makeItem(id: 'n2', isRead: false), ]); fakeRepo.unreadCount = 2; await bloc.handleEvent(LoadNotifications()); await bloc.handleEvent(MarkAllNotificationsRead()); expect(bloc.state.unreadCount, 0); expect(bloc.state.items.every((i) => i.isRead), true); }); test( 'MarkAllNotificationsRead does not update state when request fails', () async { fakeRepo.items.addAll([ makeItem(id: 'n1', isRead: false), makeItem(id: 'n2', isRead: false), ]); fakeRepo.unreadCount = 2; fakeRepo.failMarkAllRead = true; await bloc.handleEvent(LoadNotifications()); await bloc.handleEvent(RefreshUnreadCount()); await bloc.handleEvent(MarkAllNotificationsRead()); expect(bloc.state.unreadCount, 2); expect(bloc.state.items.every((i) => !i.isRead), true); }, ); test( 'NotificationCreatedEvent adds item and increments unreadCount', () async { final item = makeItem(id: 'new-1', isRead: false); bloc.handleEvent(NotificationCreatedEvent(item: item)); expect(bloc.state.items.length, 1); expect(bloc.state.unreadCount, 1); }, ); test( 'NotificationCreatedEvent for read item does not increment unreadCount', () async { final item = makeItem(id: 'new-1', isRead: true); bloc.handleEvent(NotificationCreatedEvent(item: item)); expect(bloc.state.items.length, 1); expect(bloc.state.unreadCount, 0); }, ); test( 'NotificationReadUpdatedEvent updates existing item to read', () async { fakeRepo.items.add(makeItem(id: 'n1', isRead: false)); fakeRepo.unreadCount = 1; await bloc.handleEvent(LoadNotifications()); bloc.handleEvent( NotificationReadUpdatedEvent(notificationId: 'n1', isRead: true), ); expect(bloc.state.items.first.isRead, true); expect(bloc.state.unreadCount, 0); }, ); test('NotificationRevokedEvent removes item', () async { fakeRepo.items.add( makeItem(id: 'n1', notificationId: 'notif-1', isRead: false), ); fakeRepo.unreadCount = 1; await bloc.handleEvent(LoadNotifications()); bloc.handleEvent(NotificationRevokedEvent(notificationId: 'notif-1')); expect(bloc.state.items, isEmpty); expect(bloc.state.unreadCount, 0); }); }); }