Files
familienarchiv/frontend/src/routes/persons/[id]/edit/PersonEditForm.svelte.test.ts
Marcel 577dd3fcb1 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>
2026-05-28 15:55:25 +02:00

161 lines
5.8 KiB
TypeScript

import { describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import PersonEditForm from './PersonEditForm.svelte';
afterEach(cleanup);
const personPersonal = {
id: 'p1',
personType: 'PERSON',
title: 'Frau Dr.',
firstName: 'Anna',
lastName: 'Schmidt',
alias: 'Anni',
birthYear: 1899 as number | null,
deathYear: 1972 as number | null,
notes: 'Wohnte in Berlin.'
};
const personInstitution = {
id: 'p2',
personType: 'INSTITUTION',
title: null,
firstName: null,
lastName: 'Acme GmbH',
alias: null,
birthYear: null,
deathYear: null,
notes: null
};
describe('PersonEditForm', () => {
it('renders the firstName input for the PERSON personType', async () => {
render(PersonEditForm, { props: { person: personPersonal } });
await expect.element(page.getByLabelText(/vorname/i)).toBeVisible();
});
it('renders the title input only for the PERSON personType', async () => {
render(PersonEditForm, { props: { person: personPersonal } });
await expect.element(page.getByLabelText(/titel/i)).toBeVisible();
});
it('hides the firstName / title / alias / year fields for INSTITUTION', async () => {
render(PersonEditForm, { props: { person: personInstitution } });
await expect.element(page.getByLabelText(/vorname/i)).not.toBeInTheDocument();
await expect.element(page.getByLabelText(/^titel$/i)).not.toBeInTheDocument();
await expect.element(page.getByLabelText(/rufname/i)).not.toBeInTheDocument();
await expect.element(page.getByLabelText(/geburtsjahr/i)).not.toBeInTheDocument();
await expect.element(page.getByLabelText(/todesjahr/i)).not.toBeInTheDocument();
});
it('uses the "Nachname" label for PERSON', async () => {
render(PersonEditForm, { props: { person: personPersonal } });
await expect.element(page.getByLabelText(/nachname \*/i)).toBeVisible();
});
it('uses the "Name" label for INSTITUTION', async () => {
render(PersonEditForm, { props: { person: personInstitution } });
await expect.element(page.getByLabelText(/^name \*$/i)).toBeVisible();
});
it('hydrates inputs from the person prop', async () => {
render(PersonEditForm, { props: { person: personPersonal } });
const firstName = (await page.getByLabelText(/vorname/i).element()) as HTMLInputElement;
const lastName = (await page.getByLabelText(/nachname/i).element()) as HTMLInputElement;
const alias = (await page.getByLabelText(/rufname/i).element()) as HTMLInputElement;
const title = (await page.getByLabelText(/^titel/i).element()) as HTMLInputElement;
expect(firstName.value).toBe('Anna');
expect(lastName.value).toBe('Schmidt');
expect(alias.value).toBe('Anni');
expect(title.value).toBe('Frau Dr.');
});
it('renders birthYear and deathYear inputs with prior values', async () => {
render(PersonEditForm, { props: { person: personPersonal } });
const birthYear = (await page.getByLabelText(/geburtsjahr/i).element()) as HTMLInputElement;
const deathYear = (await page.getByLabelText(/todesjahr/i).element()) as HTMLInputElement;
expect(birthYear.value).toBe('1899');
expect(deathYear.value).toBe('1972');
});
it('renders the notes textarea pre-filled with prior content', async () => {
render(PersonEditForm, { props: { person: personPersonal } });
const notes = (await page.getByLabelText(/notizen/i).element()) as HTMLTextAreaElement;
expect(notes.value).toBe('Wohnte in Berlin.');
});
it('falls back to PERSON when an unknown personType is supplied', async () => {
render(PersonEditForm, {
props: { person: { ...personPersonal, personType: 'NOT_A_TYPE' } }
});
await expect.element(page.getByLabelText(/vorname/i)).toBeVisible();
});
it('renders empty inputs when nullable fields are null', async () => {
render(PersonEditForm, {
props: { person: { ...personPersonal, title: null, alias: null, birthYear: null } }
});
const title = (await page.getByLabelText(/^titel/i).element()) as HTMLInputElement;
const alias = (await page.getByLabelText(/rufname/i).element()) as HTMLInputElement;
const birthYear = (await page.getByLabelText(/geburtsjahr/i).element()) as HTMLInputElement;
expect(title.value).toBe('');
expect(alias.value).toBe('');
expect(birthYear.value).toBe('');
});
// ─── generation dropdown (#689) ─────────────────────────────────────────────
it('renders the generation select with G 0…G 6 options when personType is PERSON', async () => {
render(PersonEditForm, { props: { person: personPersonal } });
const select = (await page.getByLabelText(/^generation$/i).element()) as HTMLSelectElement;
const labels = Array.from(select.options).map((o) => o.label.trim());
expect(labels).toEqual(
expect.arrayContaining(['G 0', 'G 1', 'G 2', 'G 3', 'G 4', 'G 5', 'G 6'])
);
});
it('hides the generation select for INSTITUTION', async () => {
render(PersonEditForm, { props: { person: personInstitution } });
await expect.element(page.getByLabelText(/^generation$/i)).not.toBeInTheDocument();
});
it('hydrates the generation select from person.generation', async () => {
render(PersonEditForm, {
props: {
person: { ...personPersonal, generation: 3 } as typeof personPersonal & {
generation: number;
}
}
});
const select = (await page.getByLabelText(/^generation$/i).element()) as HTMLSelectElement;
expect(select.value).toBe('3');
});
it('hydrates the generation select to "" when person.generation is null', async () => {
render(PersonEditForm, {
props: {
person: { ...personPersonal, generation: null } as typeof personPersonal & {
generation: number | null;
}
}
});
const select = (await page.getByLabelText(/^generation$/i).element()) as HTMLSelectElement;
expect(select.value).toBe('');
});
});