diff --git a/frontend/src/lib/notification/NotificationBell.svelte.spec.ts b/frontend/src/lib/notification/NotificationBell.svelte.spec.ts index 28c09577..6b406258 100644 --- a/frontend/src/lib/notification/NotificationBell.svelte.spec.ts +++ b/frontend/src/lib/notification/NotificationBell.svelte.spec.ts @@ -30,11 +30,12 @@ class NoopEventSource { beforeEach(() => { vi.stubGlobal('EventSource', NoopEventSource); + // init()'s fetchUnreadCount() never settles, so the bell's announced count is + // driven solely by setNotifications() — removes the init-fetch-vs-setNotifications + // race (the real fetch would resolve to {count:0} and clobber the seeded count). vi.stubGlobal( 'fetch', - vi - .fn() - .mockResolvedValue(new Response(JSON.stringify({ count: 0, content: [] }), { status: 200 })) + vi.fn(() => new Promise(() => {})) ); }); @@ -89,7 +90,6 @@ describe('NotificationBell — rendering', () => { describe('NotificationBell — announced unread count across a11y states', () => { it('empty: announces no unread count and hides the live badge', async () => { const { setNotifications } = renderBell(); - await tick(); setNotifications([]); await tick(); @@ -101,7 +101,6 @@ describe('NotificationBell — announced unread count across a11y states', () => it('single: announces one unread notification', async () => { const { setNotifications } = renderBell(); - await tick(); setNotifications([makeNotification({ id: 'n1', read: false })]); await tick(); @@ -114,7 +113,6 @@ describe('NotificationBell — announced unread count across a11y states', () => it('many: announces the exact unread count', async () => { const { setNotifications } = renderBell(); - await tick(); setNotifications([ makeNotification({ id: 'n1', read: false }), makeNotification({ id: 'n2', read: false }), @@ -128,16 +126,21 @@ describe('NotificationBell — announced unread count across a11y states', () => expect(unreadBadge().textContent?.trim()).toBe('3'); }); - it('error: degrades to a zero announced count when the unread-count load fails', async () => { + it('error: a failed unread-count load does not wipe the announced count', async () => { vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down'))); - renderBell(); + const { setNotifications } = renderBell(); + setNotifications([ + makeNotification({ id: 'n1', read: false }), + makeNotification({ id: 'n2', read: false }) + ]); await tick(); - // A failed load leaves the count at 0; the bell still announces a valid, - // non-urgent state rather than a broken count. + // init()'s fetchUnreadCount rejects; its catch must leave the already-set + // count intact rather than silently zeroing the live region. This is a + // distinct state from "empty" — the count survived a transient load failure. const btn = bellButton(); - expect(btn.getAttribute('title')).toBe(m.notification_bell_label()); + expect(btn.getAttribute('title')).toBe(m.notification_bell_unread_label({ count: 2 })); expect(btn.getAttribute('aria-label')).toBe(btn.getAttribute('title')); - expect(unreadBadge().classList.contains('hidden')).toBe(true); + expect(unreadBadge().textContent?.trim()).toBe('2'); }); });