import { afterEach, describe, it, expect, vi } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import type { NotificationItem } from '$lib/utils/notifications'; import NotificationBell from './NotificationBell.svelte'; const gotoMock = vi.hoisted(() => vi.fn()); vi.mock('$app/navigation', () => ({ goto: gotoMock, beforeNavigate: vi.fn() })); const mockMarkRead = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); const mockNotificationList = vi.hoisted((): { value: NotificationItem[] } => ({ value: [] })); vi.mock('$lib/stores/notifications.svelte', () => ({ notificationStore: { get notifications() { return mockNotificationList.value; }, get unreadCount() { return mockNotificationList.value.length; }, markRead: mockMarkRead, fetchNotifications: vi.fn().mockResolvedValue(undefined), init: vi.fn(), destroy: vi.fn(), markAllRead: vi.fn() } })); afterEach(() => { cleanup(); gotoMock.mockClear(); mockMarkRead.mockClear(); mockNotificationList.value = []; }); const makeNotification = (overrides: Partial = {}): NotificationItem => ({ id: 'n1', type: 'REPLY', documentId: 'doc-1', referenceId: 'ref-1', annotationId: null, read: false, createdAt: '2026-04-21T10:00:00Z', actorName: 'Anna', documentTitle: 'Test Doc', ...overrides }); async function openDropdownAndClickFirstNotification() { const bellButton = document.querySelector('button[aria-haspopup="true"]')!; bellButton.click(); await vi.waitFor(() => { expect(document.querySelector('[role="dialog"]')).not.toBeNull(); }); const notifButton = document.querySelector('[role="list"] button')!; notifButton.click(); } describe('NotificationBell — cursor and tooltip', () => { it('bell button has cursor-pointer class', async () => { render(NotificationBell); const btn = document.querySelector('button[aria-haspopup="true"]')!; expect(btn.classList.contains('cursor-pointer')).toBe(true); }); it('bell button title equals aria-label when unreadCount is 0', async () => { mockNotificationList.value = []; render(NotificationBell); const btn = document.querySelector('button[aria-haspopup="true"]')!; expect(btn.getAttribute('title')).toBe('Benachrichtigungen'); expect(btn.getAttribute('aria-label')).toBe(btn.getAttribute('title')); }); it('bell button title equals aria-label when unreadCount is 3', async () => { mockNotificationList.value = [ makeNotification({ id: 'n1' }), makeNotification({ id: 'n2' }), makeNotification({ id: 'n3' }) ]; render(NotificationBell); const btn = document.querySelector('button[aria-haspopup="true"]')!; expect(btn.getAttribute('title')).toBe('3 ungelesene Benachrichtigungen'); expect(btn.getAttribute('aria-label')).toBe(btn.getAttribute('title')); }); }); describe('NotificationBell', () => { it('handleMarkRead navigates to URL including annotationId when notification has annotationId', async () => { mockNotificationList.value = [makeNotification({ annotationId: 'annot-1' })]; render(NotificationBell); await openDropdownAndClickFirstNotification(); await vi.waitFor(() => { expect(gotoMock).toHaveBeenCalledWith( '/documents/doc-1?commentId=ref-1&annotationId=annot-1' ); }); }); it('handleMarkRead navigates to commentId-only URL when annotationId is absent', async () => { mockNotificationList.value = [makeNotification({ annotationId: null })]; render(NotificationBell); await openDropdownAndClickFirstNotification(); await vi.waitFor(() => { expect(gotoMock).toHaveBeenCalledWith('/documents/doc-1?commentId=ref-1'); }); }); });