import { beforeEach, describe, expect, it, vi } from 'vitest'; import { load, actions } from './+page.server'; const mockApi = { GET: vi.fn(), PATCH: vi.fn(), POST: vi.fn() }; vi.mock('$lib/shared/api.server', () => ({ createApiClient: () => mockApi })); function buildUrl(search = ''): URL { return new URL(`http://localhost/aktivitaeten${search}`); } function mockSuccess() { mockApi.GET.mockImplementation((path: string) => { if (path === '/api/dashboard/activity') { return Promise.resolve({ response: { ok: true }, data: [] }); } return Promise.resolve({ response: { ok: true }, data: { content: [] } }); }); } beforeEach(() => { vi.clearAllMocks(); }); describe('aktivitaeten/load — core', () => { it('requests only unread notifications for Für-dich', async () => { mockSuccess(); await load({ fetch, request: new Request('http://localhost/aktivitaeten'), url: buildUrl() } as never); expect(mockApi.GET).toHaveBeenCalledWith('/api/notifications', { params: { query: { read: false, page: 0, size: 20 } } }); }); it('returns the activity feed and unread notifications on success', async () => { const feed = [{ kind: 'FILE_UPLOADED', documentId: 'd1' }]; const unread = [{ id: 'n1', type: 'MENTION' }]; mockApi.GET.mockImplementation((path: string) => { if (path === '/api/dashboard/activity') { return Promise.resolve({ response: { ok: true }, data: feed }); } return Promise.resolve({ response: { ok: true }, data: { content: unread } }); }); const result = await load({ fetch, request: new Request('http://localhost/aktivitaeten'), url: buildUrl() } as never); expect(result.activityFeed).toEqual(feed); expect(result.unreadNotifications).toEqual(unread); expect(result.filter).toBe('alle'); expect(result.loadError).toBeNull(); }); it('surfaces "activity" loadError when the dashboard endpoint returns non-ok', async () => { mockApi.GET.mockImplementation((path: string) => { if (path === '/api/dashboard/activity') { return Promise.resolve({ response: { ok: false, status: 500 }, error: {} }); } return Promise.resolve({ response: { ok: true }, data: { content: [] } }); }); const result = await load({ fetch, request: new Request('http://localhost/aktivitaeten'), url: buildUrl() } as never); expect(result.loadError).toBe('activity'); expect(result.activityFeed).toEqual([]); }); it('parses the filter query param, falling back to "alle" for invalid values', async () => { mockApi.GET.mockResolvedValue({ response: { ok: true }, data: [] }); const validResult = await load({ fetch, request: new Request('http://localhost/aktivitaeten'), url: buildUrl('?filter=fuer-dich') } as never); expect(validResult.filter).toBe('fuer-dich'); mockApi.GET.mockResolvedValue({ response: { ok: true }, data: [] }); const invalidResult = await load({ fetch, request: new Request('http://localhost/aktivitaeten'), url: buildUrl('?filter=bogus') } as never); expect(invalidResult.filter).toBe('alle'); }); }); describe('aktivitaeten/load — kinds param per filter', () => { it('omits kinds for filter=alle (server defaults to ROLLUP_ELIGIBLE)', async () => { mockSuccess(); await load({ fetch, request: new Request('http://localhost/aktivitaeten'), url: buildUrl() } as never); expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { params: { query: { limit: 40 } } }); }); it('omits kinds for filter=fuer-dich (client-side filtering on youMentioned/youParticipated)', async () => { mockSuccess(); await load({ fetch, request: new Request('http://localhost/aktivitaeten'), url: buildUrl('?filter=fuer-dich') } as never); expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { params: { query: { limit: 40 } } }); }); it('sends kinds=FILE_UPLOADED for filter=hochgeladen', async () => { mockSuccess(); await load({ fetch, request: new Request('http://localhost/aktivitaeten'), url: buildUrl('?filter=hochgeladen') } as never); expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { params: { query: { limit: 40, kinds: ['FILE_UPLOADED'] } } }); }); it('sends TEXT_SAVED, BLOCK_REVIEWED, ANNOTATION_CREATED for filter=transkription', async () => { mockSuccess(); await load({ fetch, request: new Request('http://localhost/aktivitaeten'), url: buildUrl('?filter=transkription') } as never); expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { params: { query: { limit: 40, kinds: expect.arrayContaining(['TEXT_SAVED', 'BLOCK_REVIEWED', 'ANNOTATION_CREATED']) } } }); const call = mockApi.GET.mock.calls.find(([p]) => p === '/api/dashboard/activity'); expect(call[1].params.query.kinds).toHaveLength(3); }); it('sends COMMENT_ADDED, MENTION_CREATED for filter=kommentare', async () => { mockSuccess(); await load({ fetch, request: new Request('http://localhost/aktivitaeten'), url: buildUrl('?filter=kommentare') } as never); expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { params: { query: { limit: 40, kinds: expect.arrayContaining(['COMMENT_ADDED', 'MENTION_CREATED']) } } }); const call = mockApi.GET.mock.calls.find(([p]) => p === '/api/dashboard/activity'); expect(call[1].params.query.kinds).toHaveLength(2); }); }); // eslint-disable-next-line @typescript-eslint/no-explicit-any function makeActionEvent(formData: FormData): any { return { request: new Request('http://localhost/aktivitaeten', { method: 'POST', body: formData }), fetch }; } describe('aktivitaeten/actions — dismiss-notification', () => { it('returns fail(400, { error }) and does NOT call PATCH when notificationId is missing', async () => { const result = await actions['dismiss-notification'](makeActionEvent(new FormData())); expect(result).toMatchObject({ status: 400 }); expect(mockApi.PATCH).not.toHaveBeenCalled(); }); it('calls PATCH /api/notifications/{id}/read with the form-supplied notificationId', async () => { mockApi.PATCH.mockResolvedValue({ response: { ok: true }, data: {} }); const fd = new FormData(); fd.set('notificationId', 'n-abc'); await actions['dismiss-notification'](makeActionEvent(fd)); expect(mockApi.PATCH).toHaveBeenCalledWith('/api/notifications/{id}/read', { params: { path: { id: 'n-abc' } } }); }); it('returns { success: true } when the API responds ok', async () => { mockApi.PATCH.mockResolvedValue({ response: { ok: true }, data: {} }); const fd = new FormData(); fd.set('notificationId', 'n-abc'); const result = await actions['dismiss-notification'](makeActionEvent(fd)); expect(result).toEqual({ success: true }); }); it('returns fail(status, { error }) when the API responds non-ok', async () => { mockApi.PATCH.mockResolvedValue({ response: { ok: false, status: 403 }, error: { code: 'NOTIFICATION_NOT_FOUND' } }); const fd = new FormData(); fd.set('notificationId', 'n-abc'); const result = await actions['dismiss-notification'](makeActionEvent(fd)); expect(result).toMatchObject({ status: 403 }); }); }); describe('aktivitaeten/actions — mark-all-read', () => { it('calls POST /api/notifications/read-all', async () => { mockApi.POST.mockResolvedValue({ response: { ok: true }, data: null }); await actions['mark-all-read'](makeActionEvent(new FormData())); expect(mockApi.POST).toHaveBeenCalledWith('/api/notifications/read-all'); }); it('returns { success: true } when the API responds ok', async () => { mockApi.POST.mockResolvedValue({ response: { ok: true }, data: null }); const result = await actions['mark-all-read'](makeActionEvent(new FormData())); expect(result).toEqual({ success: true }); }); it('returns fail(status, { error }) when the API responds non-ok', async () => { mockApi.POST.mockResolvedValue({ response: { ok: false, status: 500 }, error: { code: 'INTERNAL_ERROR' } }); const result = await actions['mark-all-read'](makeActionEvent(new FormData())); expect(result).toMatchObject({ status: 500 }); }); });