Files
familienarchiv/frontend/src/routes/notifications/+page.server.spec.ts
Marcel 611fb52f09 feat(notifications): implement /notifications page with filter pills and load-more
New route with server load function (reads URL params, derives unreadCount from
the page, single API call per Sara's architecture requirement), mark-all form
action, and the full page UI: filter pills with ARIA radiogroup, notification
rows with border+dot unread indicators (WCAG 1.4.1), "Ältere laden" client-side
append, and empty state. Includes all de/en/es translation keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 14:10:41 +02:00

137 lines
4.6 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from 'vitest';
vi.mock('$lib/api.server', () => ({ createApiClient: vi.fn() }));
import { load, actions } from './+page.server';
import { createApiClient } from '$lib/api.server';
beforeEach(() => vi.clearAllMocks());
function makeUrl(params: Record<string, string> = {}) {
const url = new URL('http://localhost/notifications');
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
return url;
}
// ─── load ─────────────────────────────────────────────────────────────────────
describe('notifications page load', () => {
it('returns notifications and unreadCount from API response', async () => {
const mockGet = vi.fn().mockResolvedValueOnce({
response: { ok: true },
data: {
content: [
{ id: 'n1', read: false },
{ id: 'n2', read: true },
{ id: 'n3', read: false }
],
totalElements: 3,
totalPages: 1,
number: 0
}
});
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
typeof createApiClient
>);
const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch });
expect(result.notifications).toHaveLength(3);
expect(result.unreadCount).toBe(2);
});
it('passes type param to API when ?type=MENTION is in URL', async () => {
const mockGet = vi.fn().mockResolvedValueOnce({
response: { ok: true },
data: { content: [], totalElements: 0, totalPages: 0, number: 0 }
});
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
typeof createApiClient
>);
await load({ url: makeUrl({ type: 'MENTION' }), fetch: vi.fn() as unknown as typeof fetch });
const queryParams = mockGet.mock.calls[0][1].params.query;
expect(queryParams.type).toBe('MENTION');
});
it('passes read=false to API when ?read=false is in URL', async () => {
const mockGet = vi.fn().mockResolvedValueOnce({
response: { ok: true },
data: { content: [], totalElements: 0, totalPages: 0, number: 0 }
});
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
typeof createApiClient
>);
await load({ url: makeUrl({ read: 'false' }), fetch: vi.fn() as unknown as typeof fetch });
const queryParams = mockGet.mock.calls[0][1].params.query;
expect(queryParams.read).toBe(false);
});
it('passes no filter params when no search params present', async () => {
const mockGet = vi.fn().mockResolvedValueOnce({
response: { ok: true },
data: { content: [], totalElements: 0, totalPages: 0, number: 0 }
});
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
typeof createApiClient
>);
await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch });
const queryParams = mockGet.mock.calls[0][1].params.query;
expect(queryParams.type).toBeUndefined();
expect(queryParams.read).toBeUndefined();
});
it('calls the API exactly once — no separate round-trip for unreadCount', async () => {
const mockGet = vi.fn().mockResolvedValueOnce({
response: { ok: true },
data: { content: [], totalElements: 0, totalPages: 0, number: 0 }
});
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
typeof createApiClient
>);
await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch });
expect(mockGet).toHaveBeenCalledTimes(1);
});
it('throws 401 error when API returns 401', async () => {
const mockGet = vi.fn().mockResolvedValueOnce({
response: { ok: false, status: 401 },
data: null
});
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
typeof createApiClient
>);
await expect(
load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch })
).rejects.toMatchObject({ status: 401 });
});
});
// ─── mark-all action ──────────────────────────────────────────────────────────
describe('notifications mark-all action', () => {
it('calls POST /api/notifications/read-all and redirects', async () => {
const mockPost = vi.fn().mockResolvedValueOnce({ response: { ok: true } });
vi.mocked(createApiClient).mockReturnValue({ POST: mockPost } as ReturnType<
typeof createApiClient
>);
const markAll = actions['mark-all'] as (ctx: { fetch: typeof fetch }) => Promise<never>;
await expect(markAll({ fetch: vi.fn() as unknown as typeof fetch })).rejects.toMatchObject({
location: '/notifications'
});
expect(mockPost).toHaveBeenCalledTimes(1);
});
});