diff --git a/frontend/src/lib/notification/notifications.svelte.spec.ts b/frontend/src/lib/notification/notifications.svelte.spec.ts index f4bb1ca0..15345286 100644 --- a/frontend/src/lib/notification/notifications.svelte.spec.ts +++ b/frontend/src/lib/notification/notifications.svelte.spec.ts @@ -108,12 +108,46 @@ describe('notificationStore (singleton)', () => { expect(notificationStore.unreadCount).toBe(1); }); - it('markAllRead resets unreadCount', async () => { - mockFetch.mockResolvedValue(new Response(null, { status: 200 })); - await notificationStore.markAllRead(); + it('optimisticMarkRead marks the notification read and decrements unreadCount without fetching', () => { + notificationStore.init(); + const notification = makeNotification({ id: 'sse-1', read: false }); + lastEventSource!.simulate('notification', JSON.stringify(notification)); + mockFetch.mockReset(); // clear the fetchUnreadCount call from init - expect(mockFetch).toHaveBeenCalledWith('/api/notifications/read-all', { method: 'POST' }); + notificationStore.optimisticMarkRead('sse-1'); + + expect(notificationStore.notifications[0].read).toBe(true); expect(notificationStore.unreadCount).toBe(0); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('optimisticMarkRead on an already-read notification does not decrement unreadCount below 0', () => { + notificationStore.init(); + const notification = makeNotification({ id: 'sse-1', read: true }); + lastEventSource!.simulate('notification', JSON.stringify(notification)); + + notificationStore.optimisticMarkRead('sse-1'); + + expect(notificationStore.unreadCount).toBe(0); + }); + + it('optimisticMarkAllRead resets unreadCount and marks all notifications read without fetching', () => { + notificationStore.init(); + lastEventSource!.simulate( + 'notification', + JSON.stringify(makeNotification({ id: 'n1', read: false })) + ); + lastEventSource!.simulate( + 'notification', + JSON.stringify(makeNotification({ id: 'n2', read: false })) + ); + mockFetch.mockReset(); + + notificationStore.optimisticMarkAllRead(); + + expect(notificationStore.unreadCount).toBe(0); + expect(notificationStore.notifications.every((n) => n.read)).toBe(true); + expect(mockFetch).not.toHaveBeenCalled(); }); }); diff --git a/frontend/src/lib/notification/notifications.svelte.ts b/frontend/src/lib/notification/notifications.svelte.ts index 2ac49e4e..1e52cfe3 100644 --- a/frontend/src/lib/notification/notifications.svelte.ts +++ b/frontend/src/lib/notification/notifications.svelte.ts @@ -35,28 +35,19 @@ async function fetchUnreadCount(): Promise { } } -async function markRead(notification: NotificationItem): Promise { - if (!notification.read) { - try { - await fetch(`/api/notifications/${notification.id}/read`, { method: 'PATCH' }); - notification.read = true; - unreadCount = Math.max(0, unreadCount - 1); - } catch (e) { - console.error('Failed to mark notification as read', e); - } +function optimisticMarkRead(id: string): void { + const notification = notifications.find((n) => n.id === id); + if (notification && !notification.read) { + notification.read = true; + unreadCount = Math.max(0, unreadCount - 1); } } -async function markAllRead(): Promise { - try { - await fetch('/api/notifications/read-all', { method: 'POST' }); - for (const n of notifications) { - n.read = true; - } - unreadCount = 0; - } catch (e) { - console.error('Failed to mark all notifications as read', e); +function optimisticMarkAllRead(): void { + for (const n of notifications) { + n.read = true; } + unreadCount = 0; } function init(): void { @@ -123,8 +114,8 @@ export const notificationStore = { }, fetchNotifications, fetchUnreadCount, - markRead, - markAllRead, + optimisticMarkRead, + optimisticMarkAllRead, init, destroy };