import { describe, it, expect, vi, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page, userEvent } from 'vitest/browser'; import ChronikFuerDichBox from './ChronikFuerDichBox.svelte'; import type { NotificationItem } from '$lib/notification/notifications.svelte'; const mockFormResult = vi.hoisted(() => ({ type: 'success' as string })); vi.mock('$app/forms', () => ({ enhance( node: HTMLFormElement, submit?: (opts: { formData: FormData; }) => (opts: { result: { type: string; data?: Record }; update: () => Promise; }) => Promise ) { const handler = async (e: Event) => { e.preventDefault(); const cb = submit?.({ formData: new FormData(node) } as never); if (typeof cb === 'function') { await ( cb as (o: { result: typeof mockFormResult; update: () => Promise }) => Promise )({ result: mockFormResult, update: async () => {} }); } }; node.addEventListener('submit', handler); return { destroy: () => node.removeEventListener('submit', handler) }; } })); afterEach(() => { cleanup(); mockFormResult.type = 'success'; }); function notif(partial: Partial): NotificationItem { return { id: 'n1', type: 'MENTION', documentId: 'doc-1', documentTitle: 'Ein Dokument', referenceId: 'ref-1', annotationId: null, read: false, createdAt: new Date(Date.now() - 5 * 60_000).toISOString(), actorName: 'Anna', ...partial }; } describe('ChronikFuerDichBox', () => { it('renders inbox-zero state when there are no unread items', async () => { render(ChronikFuerDichBox, { unread: [], optimisticMarkRead: vi.fn(), optimisticMarkAllRead: vi.fn() }); const zero = document.querySelector('[data-testid="chronik-inbox-zero"]'); expect(zero).not.toBeNull(); await expect.element(page.getByText('Keine neuen Erwähnungen')).toBeInTheDocument(); }); it('links to the archived mentions in the inbox-zero state', async () => { render(ChronikFuerDichBox, { unread: [], optimisticMarkRead: vi.fn(), optimisticMarkAllRead: vi.fn() }); const link = document.querySelector('a[href="/aktivitaeten?filter=fuer-dich"]'); expect(link).not.toBeNull(); }); it('renders the count badge with correct total when unread exists', async () => { render(ChronikFuerDichBox, { unread: [notif({ id: 'a' }), notif({ id: 'b' })], optimisticMarkRead: vi.fn(), optimisticMarkAllRead: vi.fn() }); await expect.element(page.getByText('2 neu')).toBeInTheDocument(); }); it('count badge has aria-live=polite when unread exists', async () => { render(ChronikFuerDichBox, { unread: [notif({ id: 'a' })], optimisticMarkRead: vi.fn(), optimisticMarkAllRead: vi.fn() }); // Wait for render await expect.element(page.getByText('1 neu')).toBeInTheDocument(); const badge = document.querySelector('[data-testid="chronik-fuerdich-count"]'); expect(badge?.getAttribute('aria-live')).toBe('polite'); expect(badge?.getAttribute('aria-atomic')).toBe('true'); }); it('does not render the "Alle gelesen" button when there are no unread items', async () => { render(ChronikFuerDichBox, { unread: [], optimisticMarkRead: vi.fn(), optimisticMarkAllRead: vi.fn() }); await expect.element(page.getByText('Keine neuen Erwähnungen')).toBeInTheDocument(); const all = document.querySelector('[data-testid="chronik-mark-all-read"]'); expect(all).toBeNull(); }); it('renders the "Alle gelesen" button when unread exists', async () => { render(ChronikFuerDichBox, { unread: [notif({ id: 'a' })], optimisticMarkRead: vi.fn(), optimisticMarkAllRead: vi.fn() }); await expect.element(page.getByText('Alle gelesen')).toBeInTheDocument(); }); it('calls optimisticMarkAllRead when the "Alle gelesen" button is submitted', async () => { const optimisticMarkAllRead = vi.fn(); render(ChronikFuerDichBox, { unread: [notif({ id: 'a' })], optimisticMarkRead: vi.fn(), optimisticMarkAllRead }); await userEvent.click(page.getByText('Alle gelesen')); expect(optimisticMarkAllRead).toHaveBeenCalledTimes(1); }); it('calls optimisticMarkRead with the notification id when its dismiss button is submitted', async () => { const optimisticMarkRead = vi.fn(); const n = notif({ id: 'xyz' }); render(ChronikFuerDichBox, { unread: [n], optimisticMarkRead, optimisticMarkAllRead: vi.fn() }); const dismiss = document.querySelector( '[data-testid="chronik-fuerdich-dismiss"]' ) as HTMLButtonElement | null; expect(dismiss).not.toBeNull(); dismiss?.click(); expect(optimisticMarkRead).toHaveBeenCalledTimes(1); expect(optimisticMarkRead.mock.calls[0][0]).toBe('xyz'); }); it('mention row href includes both commentId and annotationId when annotationId is present', async () => { render(ChronikFuerDichBox, { unread: [ notif({ id: 'n-link', documentId: 'doc-42', referenceId: 'comment-7', annotationId: 'annot-9' }) ], optimisticMarkRead: vi.fn(), optimisticMarkAllRead: vi.fn() }); const link = document.querySelector( 'a[href="/documents/doc-42?commentId=comment-7&annotationId=annot-9"]' ); expect(link).not.toBeNull(); }); it('Dismiss button is a sibling of the document link, never nested inside ', async () => { render(ChronikFuerDichBox, { unread: [notif({ id: 'x' })], optimisticMarkRead: vi.fn(), optimisticMarkAllRead: vi.fn() }); const dismiss = document.querySelector('[data-testid="chronik-fuerdich-dismiss"]'); expect(dismiss).not.toBeNull(); // HTML spec forbids interactive content descendants of . // Prevents the senior-audience tap-drag bug flagged by Leonie. expect(dismiss?.closest('a')).toBeNull(); }); it('shows an accessible error banner when the dismiss action returns a failure', async () => { mockFormResult.type = 'failure'; render(ChronikFuerDichBox, { unread: [notif({ id: 'err-1' })], optimisticMarkRead: vi.fn(), optimisticMarkAllRead: vi.fn() }); const dismiss = document.querySelector( '[data-testid="chronik-fuerdich-dismiss"]' ) as HTMLButtonElement | null; expect(dismiss).not.toBeNull(); dismiss?.click(); // Allow microtask queue to flush await new Promise((r) => setTimeout(r, 0)); const alert = document.querySelector('[role="alert"]'); expect(alert).not.toBeNull(); }); });