feat: integrate invite API and improve notification handling

- Add invite code display and binding functionality via API
- Fix notification unread count sync on auth state change
- Improve notification mark read with server state validation
- Add auth state listener to trigger notification refresh
- Add YaoCoinConverter for coin-to-yao type conversion
- Remove YaoLegend from divination screens (UI cleanup)
- Abbreviate relation labels in yao detail view
- Add re-register notice to account delete screen
- Update 'coins' terminology to 'points' in localization
- Fix backend points consumption to only run in CHAT mode
- Add HttpxAuthNoiseFilter to suppress auth endpoint logging
- Fix notification static_schema import path
- Add test coverage for notification bloc error handling
- Update AGENTS.md page header rules and image handling
- Delete deprecated run-dev.sh script
This commit is contained in:
qzl
2026-04-13 14:52:22 +08:00
parent da947f9f08
commit 1e22f27de2
52 changed files with 1419 additions and 307 deletions
@@ -10,6 +10,8 @@ class _FakeNotificationRepository implements NotificationRepository {
final List<NotificationItem> items = [];
int unreadCount = 0;
int markAllReadCallCount = 0;
bool failMarkRead = false;
bool failMarkAllRead = false;
@override
Future<NotificationListResult> listNotifications({
@@ -28,6 +30,9 @@ class _FakeNotificationRepository implements NotificationRepository {
@override
Future<NotificationItem> markRead({required String notificationId}) async {
if (failMarkRead) {
throw Exception('Mark read failed');
}
final idx = items.indexWhere((i) => i.id == notificationId);
if (idx == -1) {
throw Exception('Not found');
@@ -39,6 +44,9 @@ class _FakeNotificationRepository implements NotificationRepository {
@override
Future<int> markAllRead() async {
if (failMarkAllRead) {
throw Exception('Mark all read failed');
}
markAllReadCallCount++;
final count = unreadCount;
for (int i = 0; i < items.length; i++) {
@@ -99,6 +107,21 @@ void main() {
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),
@@ -112,6 +135,24 @@ void main() {
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 {