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:
@@ -175,6 +175,9 @@
|
|||||||
"person_placeholder_notes": "Biographische Hinweise, Besonderheiten…",
|
"person_placeholder_notes": "Biographische Hinweise, Besonderheiten…",
|
||||||
"person_label_birth_year": "Geburtsjahr",
|
"person_label_birth_year": "Geburtsjahr",
|
||||||
"person_label_death_year": "Todesjahr",
|
"person_label_death_year": "Todesjahr",
|
||||||
|
"person_label_generation": "Generation",
|
||||||
|
"person_option_generation_unset": "(keine)",
|
||||||
|
"person_hint_generation": "Generation in der Familie (G 0 = älteste Generation)",
|
||||||
"person_placeholder_year": "z.B. 1923",
|
"person_placeholder_year": "z.B. 1923",
|
||||||
"person_year_error": "Bitte eine vierstellige Jahreszahl eingeben",
|
"person_year_error": "Bitte eine vierstellige Jahreszahl eingeben",
|
||||||
"person_years_error_order": "Geburtsjahr muss vor dem Todesjahr liegen",
|
"person_years_error_order": "Geburtsjahr muss vor dem Todesjahr liegen",
|
||||||
@@ -1103,7 +1106,6 @@
|
|||||||
"stammbaum_relationships_heading": "Stammbaum & Beziehungen",
|
"stammbaum_relationships_heading": "Stammbaum & Beziehungen",
|
||||||
"stammbaum_zoom_in": "Vergrößern",
|
"stammbaum_zoom_in": "Vergrößern",
|
||||||
"stammbaum_zoom_out": "Verkleinern",
|
"stammbaum_zoom_out": "Verkleinern",
|
||||||
"stammbaum_generations": "Generationen",
|
|
||||||
"relation_error_duplicate": "Diese Beziehung gibt es bereits.",
|
"relation_error_duplicate": "Diese Beziehung gibt es bereits.",
|
||||||
"relation_error_circular": "Diese Beziehung würde einen Kreis erzeugen.",
|
"relation_error_circular": "Diese Beziehung würde einen Kreis erzeugen.",
|
||||||
"relation_error_self": "Eine Person kann nicht mit sich selbst verbunden werden.",
|
"relation_error_self": "Eine Person kann nicht mit sich selbst verbunden werden.",
|
||||||
|
|||||||
@@ -175,6 +175,9 @@
|
|||||||
"person_placeholder_notes": "Biographical notes, remarks…",
|
"person_placeholder_notes": "Biographical notes, remarks…",
|
||||||
"person_label_birth_year": "Birth year",
|
"person_label_birth_year": "Birth year",
|
||||||
"person_label_death_year": "Death year",
|
"person_label_death_year": "Death year",
|
||||||
|
"person_label_generation": "Generation",
|
||||||
|
"person_option_generation_unset": "(none)",
|
||||||
|
"person_hint_generation": "Generation within the family (G 0 = oldest generation)",
|
||||||
"person_placeholder_year": "e.g. 1923",
|
"person_placeholder_year": "e.g. 1923",
|
||||||
"person_year_error": "Please enter a four-digit year",
|
"person_year_error": "Please enter a four-digit year",
|
||||||
"person_years_error_order": "Birth year must be before death year",
|
"person_years_error_order": "Birth year must be before death year",
|
||||||
@@ -1103,7 +1106,6 @@
|
|||||||
"stammbaum_relationships_heading": "Family tree & relationships",
|
"stammbaum_relationships_heading": "Family tree & relationships",
|
||||||
"stammbaum_zoom_in": "Zoom in",
|
"stammbaum_zoom_in": "Zoom in",
|
||||||
"stammbaum_zoom_out": "Zoom out",
|
"stammbaum_zoom_out": "Zoom out",
|
||||||
"stammbaum_generations": "Generations",
|
|
||||||
"relation_error_duplicate": "This relationship already exists.",
|
"relation_error_duplicate": "This relationship already exists.",
|
||||||
"relation_error_circular": "This relationship would form a cycle.",
|
"relation_error_circular": "This relationship would form a cycle.",
|
||||||
"relation_error_self": "A person cannot be related to themselves.",
|
"relation_error_self": "A person cannot be related to themselves.",
|
||||||
|
|||||||
@@ -175,6 +175,9 @@
|
|||||||
"person_placeholder_notes": "Notas biográficas, observaciones…",
|
"person_placeholder_notes": "Notas biográficas, observaciones…",
|
||||||
"person_label_birth_year": "Año de nacimiento",
|
"person_label_birth_year": "Año de nacimiento",
|
||||||
"person_label_death_year": "Año de fallecimiento",
|
"person_label_death_year": "Año de fallecimiento",
|
||||||
|
"person_label_generation": "Generación",
|
||||||
|
"person_option_generation_unset": "(ninguna)",
|
||||||
|
"person_hint_generation": "Generación dentro de la familia (G 0 = generación más antigua)",
|
||||||
"person_placeholder_year": "p.ej. 1923",
|
"person_placeholder_year": "p.ej. 1923",
|
||||||
"person_year_error": "Introduzca un año de cuatro dígitos",
|
"person_year_error": "Introduzca un año de cuatro dígitos",
|
||||||
"person_years_error_order": "El año de nacimiento debe ser anterior al año de fallecimiento",
|
"person_years_error_order": "El año de nacimiento debe ser anterior al año de fallecimiento",
|
||||||
@@ -1103,7 +1106,6 @@
|
|||||||
"stammbaum_relationships_heading": "Árbol genealógico & relaciones",
|
"stammbaum_relationships_heading": "Árbol genealógico & relaciones",
|
||||||
"stammbaum_zoom_in": "Acercar",
|
"stammbaum_zoom_in": "Acercar",
|
||||||
"stammbaum_zoom_out": "Alejar",
|
"stammbaum_zoom_out": "Alejar",
|
||||||
"stammbaum_generations": "Generaciones",
|
|
||||||
"relation_error_duplicate": "Esta relación ya existe.",
|
"relation_error_duplicate": "Esta relación ya existe.",
|
||||||
"relation_error_circular": "Esta relación crearía un ciclo.",
|
"relation_error_circular": "Esta relación crearía un ciclo.",
|
||||||
"relation_error_self": "Una persona no puede estar relacionada consigo misma.",
|
"relation_error_self": "Una persona no puede estar relacionada consigo misma.",
|
||||||
|
|||||||
@@ -1667,7 +1667,7 @@ export interface components {
|
|||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
deathYear?: number;
|
deathYear?: number;
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
generation?: number;
|
generation?: number | null;
|
||||||
};
|
};
|
||||||
Person: {
|
Person: {
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
@@ -1684,7 +1684,7 @@ export interface components {
|
|||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
deathYear?: number;
|
deathYear?: number;
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
generation?: number;
|
generation?: number | null;
|
||||||
familyMember: boolean;
|
familyMember: boolean;
|
||||||
sourceRef?: string;
|
sourceRef?: string;
|
||||||
provisional: boolean;
|
provisional: boolean;
|
||||||
@@ -2290,7 +2290,7 @@ export interface components {
|
|||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
deathYear?: number;
|
deathYear?: number;
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
generation?: number;
|
generation?: number | null;
|
||||||
familyMember: boolean;
|
familyMember: boolean;
|
||||||
};
|
};
|
||||||
InferredRelationshipDTO: {
|
InferredRelationshipDTO: {
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ export const actions = {
|
|||||||
const deathYearStr = formData.get('deathYear')?.toString().trim();
|
const deathYearStr = formData.get('deathYear')?.toString().trim();
|
||||||
const birthYear = birthYearStr ? parseInt(birthYearStr, 10) : undefined;
|
const birthYear = birthYearStr ? parseInt(birthYearStr, 10) : undefined;
|
||||||
const deathYear = deathYearStr ? parseInt(deathYearStr, 10) : undefined;
|
const deathYear = deathYearStr ? parseInt(deathYearStr, 10) : undefined;
|
||||||
|
// Must NOT use the conditional-spread idiom for generation: G 0 is a
|
||||||
|
// valid family-tree-root value. The key always travels in the body so
|
||||||
|
// an explicit clear (empty option) reaches the backend as null.
|
||||||
|
const generationRaw = formData.get('generation');
|
||||||
|
const generation =
|
||||||
|
generationRaw == null || generationRaw.toString() === '' ? null : Number(generationRaw);
|
||||||
|
|
||||||
const validationKey = validatePersonFields(personType, firstName, lastName);
|
const validationKey = validatePersonFields(personType, firstName, lastName);
|
||||||
if (validationKey) {
|
if (validationKey) {
|
||||||
@@ -68,7 +74,8 @@ export const actions = {
|
|||||||
...(alias ? { alias } : {}),
|
...(alias ? { alias } : {}),
|
||||||
...(notes ? { notes } : {}),
|
...(notes ? { notes } : {}),
|
||||||
...(birthYear ? { birthYear } : {}),
|
...(birthYear ? { birthYear } : {}),
|
||||||
...(deathYear ? { deathYear } : {})
|
...(deathYear ? { deathYear } : {}),
|
||||||
|
generation
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ let selectedType = $state<PersonType>(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Match the selectedType initialiser pattern: untrack so a subsequent prop
|
||||||
|
// update (e.g. load() rerun) does not reset the user's in-progress edit.
|
||||||
|
let generationStr = $state(
|
||||||
|
untrack(() => (person.generation == null ? '' : String(person.generation)))
|
||||||
|
);
|
||||||
|
|
||||||
const isPerson = $derived(selectedType === 'PERSON');
|
const isPerson = $derived(selectedType === 'PERSON');
|
||||||
const lastNameLabel = $derived(
|
const lastNameLabel = $derived(
|
||||||
selectedType === 'INSTITUTION' || selectedType === 'GROUP'
|
selectedType === 'INSTITUTION' || selectedType === 'GROUP'
|
||||||
@@ -108,6 +114,28 @@ const inputCls =
|
|||||||
class={inputCls}
|
class={inputCls}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label for="generation" class={labelCls}>{m.person_label_generation()}</label>
|
||||||
|
<select
|
||||||
|
id="generation"
|
||||||
|
name="generation"
|
||||||
|
bind:value={generationStr}
|
||||||
|
class="block min-h-[44px] w-full rounded border border-line bg-surface px-3 py-2 font-serif text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||||
|
aria-describedby="generation-hint"
|
||||||
|
>
|
||||||
|
<option value="">{m.person_option_generation_unset()}</option>
|
||||||
|
<option value="0">G 0</option>
|
||||||
|
<option value="1">G 1</option>
|
||||||
|
<option value="2">G 2</option>
|
||||||
|
<option value="3">G 3</option>
|
||||||
|
<option value="4">G 4</option>
|
||||||
|
<option value="5">G 5</option>
|
||||||
|
<option value="6">G 6</option>
|
||||||
|
</select>
|
||||||
|
<p id="generation-hint" class="mt-1 font-sans text-xs text-ink-3">
|
||||||
|
{m.person_hint_generation()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
|
|||||||
@@ -113,4 +113,48 @@ describe('PersonEditForm', () => {
|
|||||||
expect(alias.value).toBe('');
|
expect(alias.value).toBe('');
|
||||||
expect(birthYear.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('');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -26,6 +26,12 @@ export const actions = {
|
|||||||
const birthYearStr = formData.get('birthYear')?.toString().trim();
|
const birthYearStr = formData.get('birthYear')?.toString().trim();
|
||||||
const deathYearStr = formData.get('deathYear')?.toString().trim();
|
const deathYearStr = formData.get('deathYear')?.toString().trim();
|
||||||
const notes = formData.get('notes')?.toString().trim() || undefined;
|
const notes = formData.get('notes')?.toString().trim() || undefined;
|
||||||
|
// Must NOT use the conditional-spread idiom for generation: G 0 is a
|
||||||
|
// valid family-tree-root value. Always travels in the body so an
|
||||||
|
// explicit clear (empty option) reaches the backend as null.
|
||||||
|
const generationRaw = formData.get('generation');
|
||||||
|
const generation =
|
||||||
|
generationRaw == null || generationRaw.toString() === '' ? null : Number(generationRaw);
|
||||||
|
|
||||||
const validationKey = validatePersonFields(personType, firstName, lastName);
|
const validationKey = validatePersonFields(personType, firstName, lastName);
|
||||||
if (validationKey) {
|
if (validationKey) {
|
||||||
@@ -52,7 +58,8 @@ export const actions = {
|
|||||||
...(alias ? { alias } : {}),
|
...(alias ? { alias } : {}),
|
||||||
...(birthYear ? { birthYear } : {}),
|
...(birthYear ? { birthYear } : {}),
|
||||||
...(deathYear ? { deathYear } : {}),
|
...(deathYear ? { deathYear } : {}),
|
||||||
...(notes ? { notes } : {})
|
...(notes ? { notes } : {}),
|
||||||
|
generation
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
71
frontend/src/routes/persons/new/page.server.spec.ts
Normal file
71
frontend/src/routes/persons/new/page.server.spec.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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 buildRequest(overrides: Partial<Record<string, string>> = {}): Request {
|
||||||
|
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 new Request('http://localhost/persons/new', { method: 'POST', body: fd });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('persons/new create action — generation (#689)', () => {
|
||||||
|
it('always includes generation in the POST body — even when value is 0', async () => {
|
||||||
|
const post = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ response: { ok: true, status: 200 }, data: { id: 'p-new' } });
|
||||||
|
vi.mocked(createApiClient).mockReturnValue({ POST: post } as unknown as ReturnType<
|
||||||
|
typeof createApiClient
|
||||||
|
>);
|
||||||
|
|
||||||
|
const request = buildRequest({ generation: '0' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await actions.default({ request, fetch: mockFetch } as any);
|
||||||
|
} catch {
|
||||||
|
// redirect throws on success — ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(post).toHaveBeenCalledTimes(1);
|
||||||
|
const body = post.mock.calls[0][1].body;
|
||||||
|
expect(body).toHaveProperty('generation', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends generation: null when the dropdown is left unset', async () => {
|
||||||
|
const post = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ response: { ok: true, status: 200 }, data: { id: 'p-new' } });
|
||||||
|
vi.mocked(createApiClient).mockReturnValue({ POST: post } as unknown as ReturnType<
|
||||||
|
typeof createApiClient
|
||||||
|
>);
|
||||||
|
|
||||||
|
const request = buildRequest({ generation: '' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await actions.default({ request, fetch: mockFetch } as any);
|
||||||
|
} catch {
|
||||||
|
// redirect throws on success — ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(post).toHaveBeenCalledTimes(1);
|
||||||
|
const body = post.mock.calls[0][1].body;
|
||||||
|
expect(body).toHaveProperty('generation', null);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user