diff --git a/frontend/src/lib/document/DocumentViewer.svelte.test.ts b/frontend/src/lib/document/DocumentViewer.svelte.test.ts new file mode 100644 index 00000000..a3982a0e --- /dev/null +++ b/frontend/src/lib/document/DocumentViewer.svelte.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import DocumentViewer from './DocumentViewer.svelte'; + +afterEach(cleanup); + +const baseProps = { + doc: { id: 'd1', filePath: null, contentType: null, fileHash: null }, + fileUrl: '', + isLoading: false, + error: '', + transcribeMode: false, + blockNumbers: {}, + annotationReloadKey: 0, + activeAnnotationId: null, + annotationsDimmed: false, + flashAnnotationId: null, + onAnnotationClick: () => {} +}; + +describe('DocumentViewer', () => { + it('renders the loading spinner and label when isLoading is true', async () => { + render(DocumentViewer, { props: { ...baseProps, isLoading: true } }); + + await expect.element(page.getByText('Lade Dokument...')).toBeVisible(); + }); + + it('renders the error message when error is set', async () => { + render(DocumentViewer, { props: { ...baseProps, error: 'Datei nicht verfügbar' } }); + + await expect.element(page.getByText('Datei nicht verfügbar')).toBeVisible(); + }); + + it('shows the direct-download link in the error state when filePath is present', async () => { + render(DocumentViewer, { + props: { + ...baseProps, + doc: { ...baseProps.doc, filePath: 'docs/scan.pdf' }, + error: 'Render failed' + } + }); + + await expect + .element(page.getByRole('link', { name: /direkter download/i })) + .toHaveAttribute('href', '/api/documents/d1/file'); + }); + + it('omits the direct-download link in the error state when filePath is null', async () => { + render(DocumentViewer, { props: { ...baseProps, error: 'Render failed' } }); + + await expect + .element(page.getByRole('link', { name: /direkter download/i })) + .not.toBeInTheDocument(); + }); + + it('renders the no-scan placeholder when filePath is null and there is no error', async () => { + render(DocumentViewer, { props: baseProps }); + + await expect.element(page.getByText('Kein Scan vorhanden')).toBeVisible(); + }); + + it('renders an for non-PDF content types when fileUrl is present', async () => { + render(DocumentViewer, { + props: { + ...baseProps, + doc: { ...baseProps.doc, filePath: 'docs/x.jpg', contentType: 'image/jpeg' }, + fileUrl: '/api/documents/d1/file' + } + }); + + const img = await page.getByRole('img', { name: /original-scan/i }).element(); + expect(img.getAttribute('src')).toBe('/api/documents/d1/file'); + }); +}); diff --git a/frontend/src/routes/profile/PersonalInfoForm.svelte.test.ts b/frontend/src/routes/profile/PersonalInfoForm.svelte.test.ts new file mode 100644 index 00000000..28bd56c6 --- /dev/null +++ b/frontend/src/routes/profile/PersonalInfoForm.svelte.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import PersonalInfoForm from './PersonalInfoForm.svelte'; + +afterEach(cleanup); + +describe('PersonalInfoForm', () => { + it('renders the section heading and the four labelled inputs by default', async () => { + render(PersonalInfoForm, { props: { user: null, form: null } }); + + await expect.element(page.getByRole('heading', { name: /persönliche daten/i })).toBeVisible(); + await expect.element(page.getByLabelText(/vorname/i)).toBeVisible(); + await expect.element(page.getByLabelText(/nachname/i)).toBeVisible(); + await expect.element(page.getByLabelText(/e-mail-adresse/i)).toBeVisible(); + }); + + it('hydrates inputs from the user prop', async () => { + render(PersonalInfoForm, { + props: { + user: { + firstName: 'Anna', + lastName: 'Schmidt', + email: 'anna@example.com', + contact: 'Telefon 123' + }, + form: null + } + }); + + const first = (await page.getByLabelText(/vorname/i).element()) as HTMLInputElement; + const last = (await page.getByLabelText(/nachname/i).element()) as HTMLInputElement; + const email = (await page.getByLabelText(/e-mail-adresse/i).element()) as HTMLInputElement; + expect(first.value).toBe('Anna'); + expect(last.value).toBe('Schmidt'); + expect(email.value).toBe('anna@example.com'); + }); + + it('converts the user.birthDate ISO value to German display format', async () => { + render(PersonalInfoForm, { + props: { + user: { + firstName: 'A', + lastName: 'B', + birthDate: '1923-04-15', + email: 'x@y', + contact: '' + }, + form: null + } + }); + + const dateInput = (await page.getByLabelText(/geburtsdatum/i).element()) as HTMLInputElement; + expect(dateInput.value).toBe('15.04.1923'); + }); + + it('shows the success banner when form.updateSuccess is true', async () => { + render(PersonalInfoForm, { props: { user: null, form: { updateSuccess: true } } }); + + await expect.element(page.getByText('Gespeichert.')).toBeVisible(); + }); + + it('shows the error banner with the supplied message when form.updateError is set', async () => { + render(PersonalInfoForm, { + props: { user: null, form: { updateError: 'Email-Adresse bereits vergeben' } } + }); + + await expect.element(page.getByText('Email-Adresse bereits vergeben')).toBeVisible(); + }); + + it('hides both banners when form is null', async () => { + render(PersonalInfoForm, { props: { user: null, form: null } }); + + await expect.element(page.getByText('Gespeichert.')).not.toBeInTheDocument(); + }); + + it('declares POST as the form method and routes to the updateProfile action', async () => { + render(PersonalInfoForm, { props: { user: null, form: null } }); + + const form = document.querySelector('form'); + expect(form?.getAttribute('method')).toBe('POST'); + expect(form?.getAttribute('action')).toBe('?/updateProfile'); + }); +}); diff --git a/frontend/src/routes/profile/page.svelte.test.ts b/frontend/src/routes/profile/page.svelte.test.ts new file mode 100644 index 00000000..e67918b0 --- /dev/null +++ b/frontend/src/routes/profile/page.svelte.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import ProfilePage from './+page.svelte'; + +afterEach(cleanup); + +const baseUser = { + firstName: 'Anna', + lastName: 'Schmidt', + email: 'anna@example.com', + contact: '' +}; + +describe('profile page', () => { + it('renders the page heading and back link', async () => { + render(ProfilePage, { + props: { + data: { + user: baseUser, + notificationPrefs: { notifyOnReply: false, notifyOnMention: false } + }, + form: null + } + }); + + await expect.element(page.getByRole('heading', { name: /mein profil/i })).toBeVisible(); + await expect + .element(page.getByRole('link', { name: /zurück zur übersicht/i })) + .toHaveAttribute('href', '/'); + }); + + it('disables the notification checkboxes when the user has no email', async () => { + render(ProfilePage, { + props: { + data: { + user: { ...baseUser, email: '' }, + notificationPrefs: { notifyOnReply: false, notifyOnMention: false } + }, + form: null + } + }); + + const replyCheckbox = document.querySelector('input[name="notifyOnReply"]') as HTMLInputElement; + const mentionCheckbox = document.querySelector( + 'input[name="notifyOnMention"]' + ) as HTMLInputElement; + expect(replyCheckbox.disabled).toBe(true); + expect(mentionCheckbox.disabled).toBe(true); + }); + + it('enables the notification checkboxes when the user has an email', async () => { + render(ProfilePage, { + props: { + data: { + user: baseUser, + notificationPrefs: { notifyOnReply: true, notifyOnMention: false } + }, + form: null + } + }); + + const replyCheckbox = document.querySelector('input[name="notifyOnReply"]') as HTMLInputElement; + expect(replyCheckbox.disabled).toBe(false); + expect(replyCheckbox.checked).toBe(true); + }); + + it('shows the no-email hint when the user has no email', async () => { + render(ProfilePage, { + props: { + data: { + user: { ...baseUser, email: '' }, + notificationPrefs: { notifyOnReply: false, notifyOnMention: false } + }, + form: null + } + }); + + await expect + .element( + page.getByText( + 'Bitte trage zuerst eine E-Mail-Adresse ein, um Benachrichtigungen zu erhalten.' + ) + ) + .toBeVisible(); + }); + + it('shows the prefs success banner when form.prefsSuccess is true', async () => { + render(ProfilePage, { + props: { + data: { + user: baseUser, + notificationPrefs: { notifyOnReply: false, notifyOnMention: false } + }, + form: { prefsSuccess: true } + } + }); + + const banners = document.querySelectorAll('.bg-green-50'); + expect(banners.length).toBeGreaterThan(0); + }); + + it('shows the prefs error banner with the message when form.prefsError is set', async () => { + render(ProfilePage, { + props: { + data: { + user: baseUser, + notificationPrefs: { notifyOnReply: false, notifyOnMention: false } + }, + form: { prefsError: 'Speichern fehlgeschlagen' } + } + }); + + await expect.element(page.getByText('Speichern fehlgeschlagen')).toBeVisible(); + }); + + it('falls back to false when notificationPrefs are missing', async () => { + render(ProfilePage, { + props: { data: { user: baseUser, notificationPrefs: null }, form: null } + }); + + const replyCheckbox = document.querySelector('input[name="notifyOnReply"]') as HTMLInputElement; + expect(replyCheckbox.checked).toBe(false); + }); +});