import { describe, expect, it, vi, beforeEach } from 'vitest'; vi.mock('$lib/shared/api.server', () => ({ createApiClient: vi.fn(), extractErrorCode: (e: unknown) => (e as { code?: string } | undefined)?.code })); import { createApiClient } from '$lib/shared/api.server'; import { actions } from './+page.server'; const mockFetch = vi.fn() as unknown as typeof fetch; beforeEach(() => vi.clearAllMocks()); function makeFormData(overrides: Partial> = {}): { request: Request; redirectThrown: () => unknown; } { const fd = new FormData(); fd.set('personType', 'PERSON'); fd.set('firstName', 'Hans'); fd.set('lastName', 'Müller'); for (const [k, v] of Object.entries(overrides)) { if (v == null) fd.delete(k); else fd.set(k, v); } return { request: new Request('http://localhost/persons/p1/edit', { method: 'POST', body: fd }), redirectThrown: () => {} }; } describe('persons/[id]/edit update action — generation (#689)', () => { it('always includes generation in the PUT body — even when value is 0', async () => { const put = vi .fn() .mockResolvedValue({ response: { ok: true, status: 200 }, data: { id: 'p1' } }); vi.mocked(createApiClient).mockReturnValue({ PUT: put } as unknown as ReturnType< typeof createApiClient >); const { request } = makeFormData({ generation: '0' }); try { // eslint-disable-next-line @typescript-eslint/no-explicit-any await actions.update({ request, params: { id: 'p1' }, fetch: mockFetch } as any); } catch { // redirect throws on success — ignore } expect(put).toHaveBeenCalledTimes(1); const body = put.mock.calls[0][1].body; expect(body).toHaveProperty('generation', 0); }); it('sends generation: null when the dropdown is cleared (empty option)', async () => { const put = vi .fn() .mockResolvedValue({ response: { ok: true, status: 200 }, data: { id: 'p1' } }); vi.mocked(createApiClient).mockReturnValue({ PUT: put } as unknown as ReturnType< typeof createApiClient >); const { request } = makeFormData({ generation: '' }); try { // eslint-disable-next-line @typescript-eslint/no-explicit-any await actions.update({ request, params: { id: 'p1' }, fetch: mockFetch } as any); } catch { // redirect throws on success — ignore } expect(put).toHaveBeenCalledTimes(1); const body = put.mock.calls[0][1].body; expect(body).toHaveProperty('generation', null); }); it('sends generation: 3 when the dropdown carries G 3', async () => { const put = vi .fn() .mockResolvedValue({ response: { ok: true, status: 200 }, data: { id: 'p1' } }); vi.mocked(createApiClient).mockReturnValue({ PUT: put } as unknown as ReturnType< typeof createApiClient >); const { request } = makeFormData({ generation: '3' }); try { // eslint-disable-next-line @typescript-eslint/no-explicit-any await actions.update({ request, params: { id: 'p1' }, fetch: mockFetch } as any); } catch { // redirect throws on success — ignore } expect(put).toHaveBeenCalledTimes(1); const body = put.mock.calls[0][1].body; expect(body).toHaveProperty('generation', 3); }); }); describe('persons/[id]/edit relationship actions (#837)', () => { function relForm(overrides: Record = {}): Request { const fd = new FormData(); fd.set('relatedPersonId', 'p2'); fd.set('relationType', 'SPOUSE_OF'); for (const [k, v] of Object.entries(overrides)) { if (v == null) fd.delete(k); else fd.set(k, v); } return new Request('http://localhost/persons/p1/edit', { method: 'POST', body: fd }); } it('addRelationship posts date + precision + notes', async () => { const post = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: {} }); vi.mocked(createApiClient).mockReturnValue({ POST: post } as unknown as ReturnType< typeof createApiClient >); const request = relForm({ fromDate: '1923-05-12', fromDatePrecision: 'DAY', notes: 'Hochzeit' }); // eslint-disable-next-line @typescript-eslint/no-explicit-any await actions.addRelationship({ request, params: { id: 'p1' }, fetch: mockFetch } as any); const [path, opts] = post.mock.calls[0]; expect(path).toBe('/api/persons/{id}/relationships'); expect(opts.body).toMatchObject({ relatedPersonId: 'p2', relationType: 'SPOUSE_OF', fromDate: '1923-05-12', fromDatePrecision: 'DAY', notes: 'Hochzeit' }); }); it('addRelationship omits precision when the date is empty (coherence)', async () => { const post = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: {} }); vi.mocked(createApiClient).mockReturnValue({ POST: post } as unknown as ReturnType< typeof createApiClient >); const request = relForm({ fromDatePrecision: 'DAY' }); // precision but no date // eslint-disable-next-line @typescript-eslint/no-explicit-any await actions.addRelationship({ request, params: { id: 'p1' }, fetch: mockFetch } as any); const body = post.mock.calls[0][1].body; expect(body).not.toHaveProperty('fromDate'); expect(body).not.toHaveProperty('fromDatePrecision'); }); it('updateRelationship PUTs to the relId path with the new body', async () => { const put = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: {} }); vi.mocked(createApiClient).mockReturnValue({ PUT: put } as unknown as ReturnType< typeof createApiClient >); const request = relForm({ relId: 'rel-9', fromDate: '1923-05-12', fromDatePrecision: 'DAY' }); // eslint-disable-next-line @typescript-eslint/no-explicit-any await actions.updateRelationship({ request, params: { id: 'p1' }, fetch: mockFetch } as any); const [path, opts] = put.mock.calls[0]; expect(path).toBe('/api/persons/{id}/relationships/{relId}'); expect(opts.params.path).toMatchObject({ id: 'p1', relId: 'rel-9' }); expect(opts.body).toMatchObject({ relatedPersonId: 'p2', relationType: 'SPOUSE_OF', fromDate: '1923-05-12', fromDatePrecision: 'DAY' }); }); it('updateRelationship surfaces a backend error as a fail', async () => { const put = vi.fn().mockResolvedValue({ response: { ok: false, status: 400 }, error: { code: 'INVALID_RELATIONSHIP_DATES' } }); vi.mocked(createApiClient).mockReturnValue({ PUT: put } as unknown as ReturnType< typeof createApiClient >); const request = relForm({ relId: 'rel-9' }); const result = (await actions.updateRelationship({ request, params: { id: 'p1' }, fetch: mockFetch // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any)) as { status: number; data: { relationshipError: string } }; expect(result.status).toBe(400); expect(result.data.relationshipError).toBeTruthy(); }); });