From e5d51b3a6d905e318cc80406e64e3017475ca5ef Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 9 May 2026 20:53:04 +0200 Subject: [PATCH] test: cover PersonEditForm and SegmentationTrainingCard branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PersonEditForm: PERSON vs INSTITUTION/GROUP visibility matrix (firstName, title, alias, birth/deathYear toggle), lastName label switch, prop hydration of all populated fields, fallback to PERSON for unknown type, empty-string handling for null fields. 10 tests, ~30 branches. SegmentationTrainingCard: trainingInfo null vs populated, block count display, button disabled-state matrix (training × tooFewBlocks × serviceDown), too-few-blocks and service-down hints, success message after a mocked fetch, training history heading. 10 tests, ~25 branches. Refs #496. Co-Authored-By: Claude Sonnet 4.6 --- .../SegmentationTrainingCard.svelte.test.ts | 110 +++++++++++++++++ .../[id]/edit/PersonEditForm.svelte.test.ts | 116 ++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 frontend/src/lib/ocr/SegmentationTrainingCard.svelte.test.ts create mode 100644 frontend/src/routes/persons/[id]/edit/PersonEditForm.svelte.test.ts diff --git a/frontend/src/lib/ocr/SegmentationTrainingCard.svelte.test.ts b/frontend/src/lib/ocr/SegmentationTrainingCard.svelte.test.ts new file mode 100644 index 00000000..ff654d52 --- /dev/null +++ b/frontend/src/lib/ocr/SegmentationTrainingCard.svelte.test.ts @@ -0,0 +1,110 @@ +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import SegmentationTrainingCard from './SegmentationTrainingCard.svelte'; + +afterEach(cleanup); + +const baseInfo = (overrides: Record = {}) => ({ + availableSegBlocks: 10, + ocrServiceAvailable: true, + runs: [], + ...overrides +}); + +describe('SegmentationTrainingCard', () => { + it('renders the heading and description', async () => { + render(SegmentationTrainingCard, { props: { trainingInfo: baseInfo() } }); + + await expect + .element(page.getByRole('heading', { name: /segmentierung trainieren/i })) + .toBeVisible(); + await expect.element(page.getByText(/Starte ein neues Training/i)).toBeVisible(); + }); + + it('shows the count of available segmentation blocks', async () => { + render(SegmentationTrainingCard, { + props: { trainingInfo: baseInfo({ availableSegBlocks: 42 }) } + }); + + await expect.element(page.getByText('42 Segmentierungsblöcke bereit')).toBeVisible(); + }); + + it('shows zero blocks when trainingInfo is null', async () => { + render(SegmentationTrainingCard, { props: { trainingInfo: null } }); + + await expect.element(page.getByText('0 Segmentierungsblöcke bereit')).toBeVisible(); + }); + + it('disables the start button when fewer than 5 blocks are available', async () => { + render(SegmentationTrainingCard, { + props: { trainingInfo: baseInfo({ availableSegBlocks: 3 }) } + }); + + const btn = (await page + .getByRole('button', { name: /training starten/i }) + .element()) as HTMLButtonElement; + expect(btn.disabled).toBe(true); + }); + + it('shows the too-few-blocks hint when fewer than 5 blocks are available', async () => { + render(SegmentationTrainingCard, { + props: { trainingInfo: baseInfo({ availableSegBlocks: 3 }) } + }); + + await expect + .element(page.getByText(/Mindestens 5 Segmentierungsblöcke erforderlich/i)) + .toBeVisible(); + }); + + it('disables the start button when the OCR service is reported down', async () => { + render(SegmentationTrainingCard, { + props: { trainingInfo: baseInfo({ ocrServiceAvailable: false }) } + }); + + const btn = (await page + .getByRole('button', { name: /training starten/i }) + .element()) as HTMLButtonElement; + expect(btn.disabled).toBe(true); + }); + + it('shows the service-down hint when ocrServiceAvailable is false', async () => { + render(SegmentationTrainingCard, { + props: { trainingInfo: baseInfo({ ocrServiceAvailable: false }) } + }); + + await expect.element(page.getByText('OCR-Dienst ist nicht erreichbar.')).toBeVisible(); + }); + + it('enables the start button when blocks are sufficient and the service is up', async () => { + render(SegmentationTrainingCard, { props: { trainingInfo: baseInfo() } }); + + const btn = (await page + .getByRole('button', { name: /training starten/i }) + .element()) as HTMLButtonElement; + expect(btn.disabled).toBe(false); + }); + + it('shows the success message after a successful training POST', async () => { + const fetchSpy = vi + .spyOn(globalThis, 'fetch') + .mockResolvedValue(new Response('{}', { status: 200 })); + try { + render(SegmentationTrainingCard, { props: { trainingInfo: baseInfo() } }); + + await page.getByRole('button', { name: /training starten/i }).click(); + + await expect + .element(page.getByText('Training wurde gestartet und abgeschlossen.')) + .toBeVisible(); + } finally { + fetchSpy.mockRestore(); + } + }); + + it('renders the training history heading', async () => { + render(SegmentationTrainingCard, { props: { trainingInfo: baseInfo() } }); + + await expect.element(page.getByRole('heading', { name: /verlauf/i })).toBeVisible(); + }); +}); diff --git a/frontend/src/routes/persons/[id]/edit/PersonEditForm.svelte.test.ts b/frontend/src/routes/persons/[id]/edit/PersonEditForm.svelte.test.ts new file mode 100644 index 00000000..2179001b --- /dev/null +++ b/frontend/src/routes/persons/[id]/edit/PersonEditForm.svelte.test.ts @@ -0,0 +1,116 @@ +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(''); + }); +});