From a76af739e561dbb83ba6552c40cf1090780ae955 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 21 Apr 2026 18:37:18 +0200 Subject: [PATCH] test(notification-bell): cover handleMarkRead annotationId and commentId-only paths Co-Authored-By: Claude Sonnet 4.6 --- .../NotificationBell.svelte.spec.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 frontend/src/lib/components/NotificationBell.svelte.spec.ts diff --git a/frontend/src/lib/components/NotificationBell.svelte.spec.ts b/frontend/src/lib/components/NotificationBell.svelte.spec.ts new file mode 100644 index 00000000..733f950d --- /dev/null +++ b/frontend/src/lib/components/NotificationBell.svelte.spec.ts @@ -0,0 +1,82 @@ +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', () => { + 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'); + }); + }); +});