refactor(notifications): extract useNotificationStream and NotificationDropdown from NotificationBell (#200)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { NotificationItem } from '../useNotificationStream.svelte';
|
||||
|
||||
// Track the last created EventSource instance
|
||||
let lastEventSource: {
|
||||
close: ReturnType<typeof vi.fn>;
|
||||
onopen: (() => void) | null;
|
||||
onerror: (() => void) | null;
|
||||
} | null = null;
|
||||
|
||||
class MockEventSource {
|
||||
onopen: (() => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
close = vi.fn();
|
||||
private listeners: Record<string, ((e: MessageEvent) => 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);
|
||||
}
|
||||
}
|
||||
|
||||
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> = {}): 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user