import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { NotificationItem } from '../useNotificationStream.svelte'; // Track the last created EventSource instance let lastEventSource: { close: ReturnType; onopen: (() => void) | null; onerror: (() => void) | null; simulate: (type: string, data: string) => void; } | null = null; class MockEventSource { onopen: (() => void) | null = null; onerror: (() => void) | null = null; close = vi.fn(); private listeners: Record void)[]> = {}; constructor() { // 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); // Import after stubs are set up const { createNotificationStream } = await import('../useNotificationStream.svelte'); beforeEach(() => { mockFetch.mockReset(); lastEventSource = null; }); function makeNotification(overrides: Partial = {}): NotificationItem { return { id: 'n1', type: 'REPLY', actorName: 'Hans', documentId: 'doc-1', referenceId: 'ref-1', annotationId: null, read: false, createdAt: new Date().toISOString(), ...overrides }; } describe('createNotificationStream', () => { it('starts with empty notifications and zero unreadCount', () => { const stream = createNotificationStream(); expect(stream.notifications).toHaveLength(0); expect(stream.unreadCount).toBe(0); }); it('fetchUnreadCount updates unreadCount from API', async () => { mockFetch.mockResolvedValueOnce(new Response(JSON.stringify({ count: 3 }), { status: 200 })); const stream = createNotificationStream(); await stream.fetchUnreadCount(); expect(stream.unreadCount).toBe(3); }); it('fetchNotifications populates notifications from API', async () => { const items = [makeNotification()]; mockFetch.mockResolvedValueOnce( new Response(JSON.stringify({ content: items }), { status: 200 }) ); const stream = createNotificationStream(); await stream.fetchNotifications(); expect(stream.notifications).toHaveLength(1); expect(stream.notifications[0].id).toBe('n1'); }); it('markRead marks notification as read and decrements unreadCount', async () => { mockFetch .mockResolvedValueOnce(new Response(JSON.stringify({ count: 2 }), { status: 200 })) .mockResolvedValueOnce(new Response(null, { status: 200 })); const stream = createNotificationStream(); await stream.fetchUnreadCount(); const notification = makeNotification({ read: false }); await stream.markRead(notification); expect(notification.read).toBe(true); expect(stream.unreadCount).toBe(1); }); it('markAllRead calls the API and resets unreadCount', async () => { mockFetch.mockResolvedValueOnce(new Response(null, { status: 200 })); const stream = createNotificationStream(); await stream.markAllRead(); expect(mockFetch).toHaveBeenCalledWith('/api/notifications/read-all', { method: 'POST' }); expect(stream.unreadCount).toBe(0); }); it('destroy closes the EventSource', async () => { mockFetch.mockResolvedValue(new Response(JSON.stringify({ count: 0 }), { status: 200 })); const stream = createNotificationStream(); stream.init(); expect(lastEventSource).not.toBeNull(); stream.destroy(); expect(lastEventSource!.close).toHaveBeenCalled(); }); it('SSE notification event prepends notification and increments unreadCount', async () => { mockFetch.mockResolvedValue(new Response(JSON.stringify({ count: 0 }), { status: 200 })); const stream = createNotificationStream(); stream.init(); const notification = makeNotification({ id: 'sse-1', read: false }); lastEventSource!.simulate('notification', JSON.stringify(notification)); expect(stream.notifications).toHaveLength(1); expect(stream.notifications[0].id).toBe('sse-1'); expect(stream.unreadCount).toBe(1); }); it('SSE notification event with read:true does not increment unreadCount', async () => { mockFetch.mockResolvedValue(new Response(JSON.stringify({ count: 0 }), { status: 200 })); const stream = createNotificationStream(); stream.init(); const notification = makeNotification({ id: 'sse-2', read: true }); lastEventSource!.simulate('notification', JSON.stringify(notification)); expect(stream.notifications).toHaveLength(1); expect(stream.unreadCount).toBe(0); }); });