import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { NotificationItem } from '$lib/utils/notifications'; let lastEventSource: MockEventSource | null = null; let eventSourceCount = 0; class MockEventSource { onopen: (() => void) | null = null; onerror: (() => void) | null = null; close = vi.fn(); private listeners: Record void)[]> = {}; constructor() { eventSourceCount += 1; // eslint-disable-next-line @typescript-eslint/no-this-alias lastEventSource = this; } addEventListener(type: string, fn: (e: MessageEvent) => void) { if (!this.listeners[type]) this.listeners[type] = []; this.listeners[type].push(fn); } simulate(type: string, data: string) { const event = new MessageEvent(type, { data }); for (const fn of this.listeners[type] ?? []) { fn(event); } } } vi.stubGlobal('EventSource', MockEventSource); const mockFetch = vi.fn(); vi.stubGlobal('fetch', mockFetch); const { notificationStore, __resetForTest } = await import('./notifications.svelte'); beforeEach(() => { mockFetch.mockReset(); mockFetch.mockResolvedValue(new Response(JSON.stringify({ count: 0 }), { status: 200 })); lastEventSource = null; eventSourceCount = 0; __resetForTest(); }); function makeNotification(overrides: Partial = {}): NotificationItem { return { id: 'n1', type: 'REPLY', actorName: 'Hans', documentId: 'doc-1', documentTitle: null, referenceId: 'ref-1', annotationId: null, read: false, createdAt: new Date().toISOString(), ...overrides }; } describe('notificationStore (singleton)', () => { it('opens a single EventSource across multiple init() calls', () => { notificationStore.init(); notificationStore.init(); notificationStore.init(); expect(eventSourceCount).toBe(1); }); it('closes the EventSource only after every init() is matched with destroy()', () => { notificationStore.init(); notificationStore.init(); const es = lastEventSource!; notificationStore.destroy(); expect(es.close).not.toHaveBeenCalled(); notificationStore.destroy(); expect(es.close).toHaveBeenCalledTimes(1); }); it('reopens a fresh EventSource after full teardown', () => { notificationStore.init(); notificationStore.destroy(); notificationStore.init(); expect(eventSourceCount).toBe(2); }); it('SSE notification event prepends notification and increments unreadCount', () => { notificationStore.init(); const notification = makeNotification({ id: 'sse-1', read: false }); lastEventSource!.simulate('notification', JSON.stringify(notification)); expect(notificationStore.notifications[0].id).toBe('sse-1'); expect(notificationStore.unreadCount).toBe(1); }); it('markAllRead resets unreadCount', async () => { mockFetch.mockResolvedValue(new Response(null, { status: 200 })); await notificationStore.markAllRead(); expect(mockFetch).toHaveBeenCalledWith('/api/notifications/read-all', { method: 'POST' }); expect(notificationStore.unreadCount).toBe(0); }); });