Files
familienarchiv/frontend/src/lib/activity/ChronikFuerDichBox.svelte.test.ts
Marcel 25b23843c9 fix(test): load shared mocks via vi.hoisted, not a static import
CI caught that vi.mock('$app/forms', () => ({ ...formsMock })) with a
static `import * as formsMock` fails: vitest hoists vi.mock above the
import, so the factory references an uninitialised binding
("no top level variables inside"). Load the shared mock module via
`const formsMock = await vi.hoisted(() => import('$mocks/...'))` instead —
the factory may reference a vi.hoisted binding, and the dynamic import runs
at collection time (not in the lazily-invoked factory), so it stays clear
of ADR-012's birpc race and the no-async-mock-factories guard. Applies to
all 5 shared-mock consumers ($app/forms x4, $app/navigation x1). Part of #560.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:22 +02:00

158 lines
4.7 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import ChronikFuerDichBox from './ChronikFuerDichBox.svelte';
import type { NotificationItem } from '$lib/notification/notifications';
const formsMock = await vi.hoisted(() => import('$mocks/$app/forms'));
vi.mock('$app/forms', () => ({ ...formsMock }));
afterEach(() => {
cleanup();
});
const mention = (overrides: Partial<NotificationItem> = {}): NotificationItem => ({
id: 'n-1',
type: 'MENTION',
documentId: 'doc-1',
referenceId: 'ref-1',
annotationId: null,
read: false,
createdAt: new Date().toISOString(),
actorName: 'Anna',
documentTitle: 'Brief 1899',
...overrides
});
describe('ChronikFuerDichBox', () => {
it('renders the inbox-zero state when there are no unread', async () => {
render(ChronikFuerDichBox, {
props: { unread: [], optimisticMarkRead: () => {}, optimisticMarkAllRead: () => {} }
});
await expect.element(page.getByText(/keine neuen erwähnungen/i)).toBeVisible();
const link = document.querySelector('a[href="/aktivitaeten?filter=fuer-dich"]');
expect(link).not.toBeNull();
});
it('renders the count badge with the unread count', async () => {
render(ChronikFuerDichBox, {
props: {
unread: [mention(), mention({ id: 'n-2' }), mention({ id: 'n-3' })],
optimisticMarkRead: () => {},
optimisticMarkAllRead: () => {}
}
});
const badge = document.querySelector('[data-testid="chronik-fuerdich-count"]');
expect(badge?.textContent).toContain('3');
});
it('uses the @ glyph for MENTION and ↩ for REPLY', async () => {
render(ChronikFuerDichBox, {
props: {
unread: [mention({ id: 'n-m', type: 'MENTION' }), mention({ id: 'n-r', type: 'REPLY' })],
optimisticMarkRead: () => {},
optimisticMarkAllRead: () => {}
}
});
const items = document.querySelectorAll('ul[role="list"] li');
expect(items.length).toBe(2);
expect(items[0].textContent).toContain('@');
expect(items[1].textContent).toContain('↩');
});
it('renders MENTION verb text from paraglide messages', async () => {
render(ChronikFuerDichBox, {
props: {
unread: [mention({ actorName: 'Bertha' })],
optimisticMarkRead: () => {},
optimisticMarkAllRead: () => {}
}
});
await expect
.element(page.getByText(/bertha hat dich in einem kommentar erwähnt/i))
.toBeVisible();
});
it('renders REPLY verb text from paraglide messages', async () => {
render(ChronikFuerDichBox, {
props: {
unread: [mention({ type: 'REPLY', actorName: 'Carl' })],
optimisticMarkRead: () => {},
optimisticMarkAllRead: () => {}
}
});
await expect
.element(page.getByText(/carl hat auf deinen kommentar geantwortet/i))
.toBeVisible();
});
it('calls optimisticMarkRead with the notification id when its dismiss button is clicked', async () => {
const optimisticMarkRead = vi.fn();
const item = mention({ id: 'n-7' });
render(ChronikFuerDichBox, {
props: { unread: [item], optimisticMarkRead, optimisticMarkAllRead: () => {} }
});
const dismiss = document.querySelector(
'[data-testid="chronik-fuerdich-dismiss"]'
) as HTMLElement;
dismiss.click();
expect(optimisticMarkRead).toHaveBeenCalledWith('n-7');
});
it('calls optimisticMarkAllRead when the mark-all-read button is clicked', async () => {
const optimisticMarkAllRead = vi.fn();
render(ChronikFuerDichBox, {
props: {
unread: [mention()],
optimisticMarkRead: () => {},
optimisticMarkAllRead
}
});
const btn = document.querySelector('[data-testid="chronik-mark-all-read"]') as HTMLElement;
btn.click();
expect(optimisticMarkAllRead).toHaveBeenCalledOnce();
});
it('builds a deep-link href to the comment for each notification', async () => {
render(ChronikFuerDichBox, {
props: {
unread: [mention({ documentId: 'doc-x', referenceId: 'ref-y', annotationId: null })],
optimisticMarkRead: () => {},
optimisticMarkAllRead: () => {}
}
});
const link = document.querySelector('ul[role="list"] li a') as HTMLAnchorElement;
expect(link.getAttribute('href')).toContain('doc-x');
});
it('shows an accessible error banner when the dismiss action returns a failure', async () => {
formsMock.setFormResult({ type: 'failure' });
render(ChronikFuerDichBox, {
props: {
unread: [mention({ id: 'err-1' })],
optimisticMarkRead: () => {},
optimisticMarkAllRead: () => {}
}
});
const dismiss = document.querySelector(
'[data-testid="chronik-fuerdich-dismiss"]'
) as HTMLElement;
dismiss.click();
// Allow microtask queue to flush
await new Promise((r) => setTimeout(r, 0));
const alert = document.querySelector('[role="alert"]');
expect(alert).not.toBeNull();
});
});