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>
137 lines
4.6 KiB
TypeScript
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);
|
|
});
|
|
});
|