feat(person): generation dropdown on Person edit/new forms (#689)
PersonEditForm.svelte gains a G 0…G 6 select inside the {#if isPerson}
block. min-h-[44px] meets WCAG 2.5.8 / dual-audience touch target.
generationStr is initialised via $state(untrack(...)) so prop reruns
never reset an in-progress edit (same pattern as selectedType).
Both /persons/[id]/edit and /persons/new form actions read the field
without the conditional-spread idiom — generation always lands in the
PUT/POST body. G 0 is a valid family-tree-root value the spread would
silently drop, and an empty option sends null so a human can clear the
field back to "unset".
i18n adds person_label_generation / person_option_generation_unset /
person_hint_generation in de/en/es. Drops the dead stammbaum_generations
key (zero callsites after the filter-chip removal in the spec).
Tests: dropdown render + hydration in the component, generation=0/3/null
arriving in the API body in the server actions.
Refs #689
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
99
frontend/src/routes/persons/[id]/edit/page.server.spec.ts
Normal file
99
frontend/src/routes/persons/[id]/edit/page.server.spec.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
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<Record<string, string>> = {}): {
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user