diff --git a/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts b/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts new file mode 100644 index 00000000..0c78212f --- /dev/null +++ b/frontend/src/lib/notification/NotificationDropdown.svelte.test.ts @@ -0,0 +1,171 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import NotificationDropdown from './NotificationDropdown.svelte'; + +afterEach(cleanup); + +const makeNotification = (overrides: Record = {}) => ({ + id: 'n1', + type: 'REPLY' as 'REPLY' | 'MENTION', + documentId: 'd1', + documentTitle: 'Brief', + referenceId: 'c1', + annotationId: null, + read: false, + createdAt: new Date().toISOString(), + actorName: 'Anna Schmidt', + ...overrides +}); + +describe('NotificationDropdown', () => { + it('renders the dialog with the bell label', async () => { + render(NotificationDropdown, { + props: { + notifications: [], + onMarkRead: () => {}, + onMarkAllRead: () => {}, + onClose: () => {} + } + }); + + await expect.element(page.getByRole('dialog', { name: /benachrichtigungen/i })).toBeVisible(); + }); + + it('renders the empty state when there are no notifications', async () => { + render(NotificationDropdown, { + props: { + notifications: [], + onMarkRead: () => {}, + onMarkAllRead: () => {}, + onClose: () => {} + } + }); + + await expect.element(page.getByText('Keine neuen Benachrichtigungen')).toBeVisible(); + }); + + it('hides the mark-all-read action when the list is empty', async () => { + render(NotificationDropdown, { + props: { + notifications: [], + onMarkRead: () => {}, + onMarkAllRead: () => {}, + onClose: () => {} + } + }); + + await expect + .element(page.getByRole('button', { name: /alle gelesen/i })) + .not.toBeInTheDocument(); + }); + + it('renders the mark-all-read action when notifications are present', async () => { + render(NotificationDropdown, { + props: { + notifications: [makeNotification()], + onMarkRead: () => {}, + onMarkAllRead: () => {}, + onClose: () => {} + } + }); + + await expect.element(page.getByRole('button', { name: /alle gelesen/i })).toBeVisible(); + }); + + it('renders one item per notification with the reply text for REPLY type', async () => { + render(NotificationDropdown, { + props: { + notifications: [makeNotification({ type: 'REPLY', actorName: 'Bert' })], + onMarkRead: () => {}, + onMarkAllRead: () => {}, + onClose: () => {} + } + }); + + await expect + .element(page.getByText(/Bert hat auf deinen Kommentar geantwortet/i)) + .toBeVisible(); + }); + + it('renders the mention text for MENTION type', async () => { + render(NotificationDropdown, { + props: { + notifications: [makeNotification({ type: 'MENTION', actorName: 'Clara' })], + onMarkRead: () => {}, + onMarkAllRead: () => {}, + onClose: () => {} + } + }); + + await expect + .element(page.getByText(/Clara hat dich in einem Kommentar erwähnt/i)) + .toBeVisible(); + }); + + it('renders the unread dot only for unread notifications', async () => { + render(NotificationDropdown, { + props: { + notifications: [ + makeNotification({ id: 'n1', read: false }), + makeNotification({ id: 'n2', read: true }) + ], + onMarkRead: () => {}, + onMarkAllRead: () => {}, + onClose: () => {} + } + }); + + const unreadDots = document.querySelectorAll('[aria-label="ungelesen"]'); + expect(unreadDots.length).toBe(1); + }); + + it('calls onMarkRead with the notification when an item is clicked', async () => { + const onMarkRead = vi.fn(); + const n = makeNotification({ id: 'n42', actorName: 'Anna' }); + render(NotificationDropdown, { + props: { + notifications: [n], + onMarkRead, + onMarkAllRead: () => {}, + onClose: () => {} + } + }); + + await page.getByRole('button', { name: /Anna hat auf deinen/i }).click(); + + expect(onMarkRead).toHaveBeenCalledWith(n); + }); + + it('calls onMarkAllRead when the mark-all-read button is clicked', async () => { + const onMarkAllRead = vi.fn(); + render(NotificationDropdown, { + props: { + notifications: [makeNotification()], + onMarkRead: () => {}, + onMarkAllRead, + onClose: () => {} + } + }); + + await page.getByRole('button', { name: /alle gelesen/i }).click(); + + expect(onMarkAllRead).toHaveBeenCalledOnce(); + }); + + it('calls onClose when the view-all link is clicked', async () => { + const onClose = vi.fn(); + render(NotificationDropdown, { + props: { + notifications: [], + onMarkRead: () => {}, + onMarkAllRead: () => {}, + onClose + } + }); + + await page.getByRole('link').click(); + + expect(onClose).toHaveBeenCalledOnce(); + }); +});