diff --git a/frontend/src/lib/ocr/OcrTrigger.svelte.test.ts b/frontend/src/lib/ocr/OcrTrigger.svelte.test.ts new file mode 100644 index 00000000..9aaed1d7 --- /dev/null +++ b/frontend/src/lib/ocr/OcrTrigger.svelte.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import OcrTrigger from './OcrTrigger.svelte'; + +afterEach(cleanup); + +describe('OcrTrigger', () => { + it('renders the script-type select and the trigger button', async () => { + render(OcrTrigger, { + props: { blockCount: 1, storedScriptType: 'HANDWRITING_KURRENT', onTrigger: () => {} } + }); + + await expect.element(page.getByRole('combobox')).toBeVisible(); + await expect.element(page.getByRole('button')).toBeVisible(); + }); + + it('initialises the select with the stored script type when provided', async () => { + render(OcrTrigger, { + props: { blockCount: 1, storedScriptType: 'HANDWRITING_KURRENT', onTrigger: () => {} } + }); + + const select = (await page.getByRole('combobox').element()) as HTMLSelectElement; + expect(select.value).toBe('HANDWRITING_KURRENT'); + }); + + it('starts with an empty selection when storedScriptType is UNKNOWN', async () => { + render(OcrTrigger, { + props: { blockCount: 1, storedScriptType: 'UNKNOWN', onTrigger: () => {} } + }); + + const select = (await page.getByRole('combobox').element()) as HTMLSelectElement; + expect(select.value).toBe(''); + }); + + it('disables the trigger button when no script type is selected', async () => { + render(OcrTrigger, { + props: { blockCount: 1, storedScriptType: 'UNKNOWN', onTrigger: () => {} } + }); + + const btn = (await page.getByRole('button').element()) as HTMLButtonElement; + expect(btn.disabled).toBe(true); + }); + + it('disables the trigger button when blockCount is 0 even if a script type is selected', async () => { + render(OcrTrigger, { + props: { blockCount: 0, storedScriptType: 'HANDWRITING_KURRENT', onTrigger: () => {} } + }); + + const btn = (await page.getByRole('button').element()) as HTMLButtonElement; + expect(btn.disabled).toBe(true); + }); + + it('shows the no-annotations hint when blockCount is 0', async () => { + render(OcrTrigger, { + props: { blockCount: 0, storedScriptType: 'HANDWRITING_KURRENT', onTrigger: () => {} } + }); + + await expect + .element(page.getByText('Zeichnen Sie zuerst Bereiche auf dem Dokument ein.')) + .toBeVisible(); + }); + + it('omits the no-annotations hint when blockCount is greater than 0', async () => { + render(OcrTrigger, { + props: { blockCount: 5, storedScriptType: 'HANDWRITING_KURRENT', onTrigger: () => {} } + }); + + await expect + .element(page.getByText('Zeichnen Sie zuerst Bereiche auf dem Dokument ein.')) + .not.toBeInTheDocument(); + }); + + it('calls onTrigger with the selected script type and useExistingAnnotations=true', async () => { + const onTrigger = vi.fn(); + render(OcrTrigger, { + props: { blockCount: 5, storedScriptType: 'HANDWRITING_KURRENT', onTrigger } + }); + + await page.getByRole('button').click(); + + expect(onTrigger).toHaveBeenCalledWith('HANDWRITING_KURRENT', true); + }); + + it('does not call onTrigger when no script type is selected', async () => { + const onTrigger = vi.fn(); + render(OcrTrigger, { + props: { blockCount: 5, storedScriptType: 'UNKNOWN', onTrigger } + }); + + await page.getByRole('button').click({ force: true }); + + expect(onTrigger).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte.test.ts b/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte.test.ts new file mode 100644 index 00000000..147523b6 --- /dev/null +++ b/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import CoCorrespondentsList from './CoCorrespondentsList.svelte'; + +afterEach(cleanup); + +describe('CoCorrespondentsList', () => { + it('renders nothing when the coCorrespondents list is empty', async () => { + render(CoCorrespondentsList, { + props: { coCorrespondents: [], personId: 'p-1' } + }); + + await expect + .element(page.getByRole('heading', { name: /häufige korrespondenten/i })) + .not.toBeInTheDocument(); + }); + + it('renders the heading and hint when there is at least one co-correspondent', async () => { + render(CoCorrespondentsList, { + props: { + coCorrespondents: [{ id: 'c-1', name: 'Max Mustermann', count: 3 }], + personId: 'p-1' + } + }); + + await expect + .element(page.getByRole('heading', { name: /häufige korrespondenten/i })) + .toBeVisible(); + await expect.element(page.getByText('klicken für Konversation')).toBeVisible(); + }); + + it('renders one chip per co-correspondent with name and count', async () => { + render(CoCorrespondentsList, { + props: { + coCorrespondents: [ + { id: 'c-1', name: 'Max Mustermann', count: 3 }, + { id: 'c-2', name: 'Erika Beispiel', count: 1 } + ], + personId: 'p-1' + } + }); + + await expect.element(page.getByText('Max Mustermann')).toBeVisible(); + await expect.element(page.getByText('Erika Beispiel')).toBeVisible(); + await expect.element(page.getByText('×3')).toBeVisible(); + await expect.element(page.getByText('×1')).toBeVisible(); + }); + + it('points each chip to the bilateral conversation route with the correct ids', async () => { + render(CoCorrespondentsList, { + props: { + coCorrespondents: [{ id: 'c-1', name: 'Max Mustermann', count: 3 }], + personId: 'p-1' + } + }); + + await expect + .element(page.getByRole('link', { name: /max mustermann/i })) + .toHaveAttribute('href', '/briefwechsel?senderId=p-1&receiverId=c-1'); + }); + + it('builds initials from up to two name parts', async () => { + render(CoCorrespondentsList, { + props: { + coCorrespondents: [{ id: 'c-1', name: 'Max Mustermann Beispiel', count: 1 }], + personId: 'p-1' + } + }); + + await expect.element(page.getByText('MM')).toBeVisible(); + }); +}); diff --git a/frontend/src/routes/reset-password/page.svelte.test.ts b/frontend/src/routes/reset-password/page.svelte.test.ts new file mode 100644 index 00000000..a9e16bc5 --- /dev/null +++ b/frontend/src/routes/reset-password/page.svelte.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import ResetPasswordPage from './+page.svelte'; + +afterEach(cleanup); + +describe('reset-password page', () => { + it('renders the password and confirmation inputs by default', async () => { + render(ResetPasswordPage, { + props: { data: { token: 'abc' }, form: undefined } + }); + + await expect + .element(page.getByRole('heading', { name: /neues passwort festlegen/i })) + .toBeVisible(); + await expect.element(page.getByLabelText('Neues Passwort')).toBeVisible(); + await expect.element(page.getByLabelText('Passwort bestätigen')).toBeVisible(); + }); + + it('renders the token in a hidden input', async () => { + render(ResetPasswordPage, { + props: { data: { token: 'tok-123' }, form: undefined } + }); + + const tokenInput = document.querySelector('input[name="token"]') as HTMLInputElement; + expect(tokenInput.type).toBe('hidden'); + expect(tokenInput.value).toBe('tok-123'); + }); + + it('falls back to an empty token when data.token is null', async () => { + render(ResetPasswordPage, { + props: { data: { token: null }, form: undefined } + }); + + const tokenInput = document.querySelector('input[name="token"]') as HTMLInputElement; + expect(tokenInput.value).toBe(''); + }); + + it('shows the success banner and hides the form when form.success is true', async () => { + render(ResetPasswordPage, { + props: { data: { token: 'abc' }, form: { success: true } } + }); + + await expect + .element( + page.getByText('Ihr Passwort wurde erfolgreich geändert. Sie können sich jetzt anmelden.') + ) + .toBeVisible(); + await expect.element(page.getByLabelText('Neues Passwort')).not.toBeInTheDocument(); + }); + + it('renders the localised mismatch message for the MISMATCH error code', async () => { + render(ResetPasswordPage, { + props: { data: { token: 'abc' }, form: { error: 'MISMATCH' } } + }); + + await expect.element(page.getByText('Die Passwörter stimmen nicht überein.')).toBeVisible(); + }); + + it('falls back to the generic error message for any other error code', async () => { + render(ResetPasswordPage, { + props: { data: { token: 'abc' }, form: { error: 'TOKEN_EXPIRED' } } + }); + + await expect + .element(page.getByText(/(fehler|expired|abgelaufen|ungültig|generic)/i)) + .toBeVisible(); + }); + + it('always offers a back-to-login link', async () => { + render(ResetPasswordPage, { + props: { data: { token: 'abc' }, form: undefined } + }); + + await expect + .element(page.getByRole('link', { name: /zurück zum login/i })) + .toHaveAttribute('href', '/login'); + }); +});