refactor(chronik): replace callback props with form actions in ChronikFuerDichBox
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m20s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m22s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 19s
CI / Compose Bucket Idempotency (pull_request) Successful in 58s

Dismiss (X) button and mark-all-read button now submit forms to
/aktivitaeten?/dismiss-notification and /aktivitaeten?/mark-all-read respectively.
Props renamed onMarkRead/onMarkAllRead → optimisticMarkRead/optimisticMarkAllRead.

aktivitaeten/+page.svelte drops the now-deleted onMarkRead/onMarkAllRead wrapper functions
and passes notificationStore.optimisticMarkRead/optimisticMarkAllRead directly to the box.

Tests: $app/forms enhance mock added to both spec files so dismiss and mark-all assertions
work synchronously against form-submit events.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-19 23:16:58 +02:00
parent 2c6b59d0c7
commit 23439e581a
4 changed files with 124 additions and 84 deletions

View File

@@ -5,6 +5,17 @@ import { page, userEvent } from 'vitest/browser';
import ChronikFuerDichBox from './ChronikFuerDichBox.svelte';
import type { NotificationItem } from '$lib/notification/notifications.svelte';
vi.mock('$app/forms', () => ({
enhance(node: HTMLFormElement, submit?: (opts: { formData: FormData }) => unknown) {
const handler = (e: Event) => {
e.preventDefault();
submit?.({ formData: new FormData(node) } as never);
};
node.addEventListener('submit', handler);
return { destroy: () => node.removeEventListener('submit', handler) };
}
}));
afterEach(cleanup);
function notif(partial: Partial<NotificationItem>): NotificationItem {
@@ -26,8 +37,8 @@ describe('ChronikFuerDichBox', () => {
it('renders inbox-zero state when there are no unread items', async () => {
render(ChronikFuerDichBox, {
unread: [],
onMarkRead: vi.fn(),
onMarkAllRead: vi.fn()
optimisticMarkRead: vi.fn(),
optimisticMarkAllRead: vi.fn()
});
const zero = document.querySelector('[data-testid="chronik-inbox-zero"]');
expect(zero).not.toBeNull();
@@ -37,8 +48,8 @@ describe('ChronikFuerDichBox', () => {
it('links to the archived mentions in the inbox-zero state', async () => {
render(ChronikFuerDichBox, {
unread: [],
onMarkRead: vi.fn(),
onMarkAllRead: vi.fn()
optimisticMarkRead: vi.fn(),
optimisticMarkAllRead: vi.fn()
});
const link = document.querySelector('a[href="/aktivitaeten?filter=fuer-dich"]');
expect(link).not.toBeNull();
@@ -47,8 +58,8 @@ describe('ChronikFuerDichBox', () => {
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()
optimisticMarkRead: vi.fn(),
optimisticMarkAllRead: vi.fn()
});
await expect.element(page.getByText('2 neu')).toBeInTheDocument();
});
@@ -56,8 +67,8 @@ describe('ChronikFuerDichBox', () => {
it('count badge has aria-live=polite when unread exists', async () => {
render(ChronikFuerDichBox, {
unread: [notif({ id: 'a' })],
onMarkRead: vi.fn(),
onMarkAllRead: vi.fn()
optimisticMarkRead: vi.fn(),
optimisticMarkAllRead: vi.fn()
});
// Wait for render
await expect.element(page.getByText('1 neu')).toBeInTheDocument();
@@ -69,8 +80,8 @@ describe('ChronikFuerDichBox', () => {
it('does not render the "Alle gelesen" button when there are no unread items', async () => {
render(ChronikFuerDichBox, {
unread: [],
onMarkRead: vi.fn(),
onMarkAllRead: vi.fn()
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"]');
@@ -80,38 +91,38 @@ describe('ChronikFuerDichBox', () => {
it('renders the "Alle gelesen" button when unread exists', async () => {
render(ChronikFuerDichBox, {
unread: [notif({ id: 'a' })],
onMarkRead: vi.fn(),
onMarkAllRead: vi.fn()
optimisticMarkRead: vi.fn(),
optimisticMarkAllRead: 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();
it('calls optimisticMarkAllRead when the "Alle gelesen" button is submitted', async () => {
const optimisticMarkAllRead = vi.fn();
render(ChronikFuerDichBox, {
unread: [notif({ id: 'a' })],
onMarkRead: vi.fn(),
onMarkAllRead
optimisticMarkRead: vi.fn(),
optimisticMarkAllRead
});
await userEvent.click(page.getByText('Alle gelesen'));
expect(onMarkAllRead).toHaveBeenCalledTimes(1);
expect(optimisticMarkAllRead).toHaveBeenCalledTimes(1);
});
it('calls onMarkRead (and not navigation) when a per-item Dismiss button is clicked', async () => {
const onMarkRead = vi.fn();
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],
onMarkRead,
onMarkAllRead: vi.fn()
optimisticMarkRead,
optimisticMarkAllRead: 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);
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 () => {
@@ -124,8 +135,8 @@ describe('ChronikFuerDichBox', () => {
annotationId: 'annot-9'
})
],
onMarkRead: vi.fn(),
onMarkAllRead: vi.fn()
optimisticMarkRead: vi.fn(),
optimisticMarkAllRead: vi.fn()
});
const link = document.querySelector(
'a[href="/documents/doc-42?commentId=comment-7&annotationId=annot-9"]'
@@ -136,8 +147,8 @@ describe('ChronikFuerDichBox', () => {
it('Dismiss button is a sibling of the document link, never nested inside <a>', async () => {
render(ChronikFuerDichBox, {
unread: [notif({ id: 'x' })],
onMarkRead: vi.fn(),
onMarkAllRead: vi.fn()
optimisticMarkRead: vi.fn(),
optimisticMarkAllRead: vi.fn()
});
const dismiss = document.querySelector('[data-testid="chronik-fuerdich-dismiss"]');
expect(dismiss).not.toBeNull();