All under src/lib/components/chronik/: - ChronikRow.svelte — single orchestrator for four variants (comment / for-you / rollup / simple), discriminated via $derived. Outer <a> wraps avatar + body + time; document title is a styled <span> (no nested anchors). Rollup shows count badge + en-dash time range; for-you gets accent left border + @ marker hidden below sm:. - ChronikTimeline.svelte — buckets items by day using bucketByDay() and renders Heute/Gestern/Diese Woche/Älter section headers with <span> trailing rule. - ChronikFuerDichBox.svelte — unread mentions card with inbox-zero variant, per-row Dismiss button (prevents bubbling, calls onMarkRead), aria-live count badge, and a .fade-in class gated by prefers-reduced-motion. - ChronikFilterPills.svelte — role=radiogroup with 5 pills, ArrowLeft/Right keyboard navigation wrapping across the group, single tabstop via dynamic tabindex. - ChronikEmptyState.svelte — three variants (first-run / filter-empty / inbox-zero) sharing a centered-column layout. - ChronikErrorCard.svelte — warning card with retry button, optional custom message override. Verbs map to chronik_singleton_* / chronik_rollup_* per AuditKind so no ICU pluralization is needed. Comment preview is a TODO placeholder (currently the document title) pending a backend preview DTO follow-up. All 40 unit tests green. No type-check or lint errors in these files. Part of #285. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
3.6 KiB
TypeScript
117 lines
3.6 KiB
TypeScript
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/stores/notifications.svelte';
|
|
|
|
afterEach(cleanup);
|
|
|
|
function notif(partial: Partial<NotificationItem>): 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: [],
|
|
onMarkRead: vi.fn(),
|
|
onMarkAllRead: 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: [],
|
|
onMarkRead: vi.fn(),
|
|
onMarkAllRead: vi.fn()
|
|
});
|
|
const link = document.querySelector('a[href="/chronik?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' })],
|
|
onMarkRead: vi.fn(),
|
|
onMarkAllRead: 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' })],
|
|
onMarkRead: vi.fn(),
|
|
onMarkAllRead: 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: [],
|
|
onMarkRead: vi.fn(),
|
|
onMarkAllRead: 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' })],
|
|
onMarkRead: vi.fn(),
|
|
onMarkAllRead: vi.fn()
|
|
});
|
|
await expect.element(page.getByText('Alle gelesen')).toBeInTheDocument();
|
|
});
|
|
|
|
it('calls onMarkAllRead when the "Alle gelesen" button is clicked', async () => {
|
|
const onMarkAllRead = vi.fn();
|
|
render(ChronikFuerDichBox, {
|
|
unread: [notif({ id: 'a' })],
|
|
onMarkRead: vi.fn(),
|
|
onMarkAllRead
|
|
});
|
|
await userEvent.click(page.getByText('Alle gelesen'));
|
|
expect(onMarkAllRead).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('calls onMarkRead (and not navigation) when a per-item Dismiss button is clicked', async () => {
|
|
const onMarkRead = vi.fn();
|
|
const n = notif({ id: 'xyz' });
|
|
render(ChronikFuerDichBox, {
|
|
unread: [n],
|
|
onMarkRead,
|
|
onMarkAllRead: vi.fn()
|
|
});
|
|
const dismiss = document.querySelector(
|
|
'[data-testid="chronik-fuerdich-dismiss"]'
|
|
) as HTMLButtonElement | null;
|
|
expect(dismiss).not.toBeNull();
|
|
dismiss?.click();
|
|
expect(onMarkRead).toHaveBeenCalledTimes(1);
|
|
expect(onMarkRead.mock.calls[0][0]).toEqual(n);
|
|
});
|
|
});
|