diff --git a/frontend/src/lib/components/DashboardMentions.svelte.spec.ts b/frontend/src/lib/components/DashboardMentions.svelte.spec.ts index c0dd67df..10643825 100644 --- a/frontend/src/lib/components/DashboardMentions.svelte.spec.ts +++ b/frontend/src/lib/components/DashboardMentions.svelte.spec.ts @@ -47,7 +47,7 @@ describe('DashboardMentions', () => { render(DashboardMentions, { mentions: [makeMention({ documentId: 'doc-1', referenceId: 'cmt-1' })] }); - const link = page.getByRole('link'); + const link = page.getByRole('link', { name: 'Anna Schmidt' }); await expect.element(link).toHaveAttribute('href', '/documents/doc-1?commentId=cmt-1'); }); @@ -55,7 +55,7 @@ describe('DashboardMentions', () => { render(DashboardMentions, { mentions: [makeMention({ documentId: 'doc-2', referenceId: 'cmt-2', annotationId: 'ann-9' })] }); - const link = page.getByRole('link'); + const link = page.getByRole('link', { name: 'Anna Schmidt' }); await expect .element(link) .toHaveAttribute('href', '/documents/doc-2?commentId=cmt-2&annotationId=ann-9'); @@ -70,7 +70,7 @@ describe('DashboardMentions', () => { render(DashboardMentions, { mentions: [makeMention({ type: 'REPLY' })] }); const widget = page.getByTestId('dashboard-mentions'); await expect.element(widget).toBeInTheDocument(); - const link = page.getByRole('link'); + const link = page.getByRole('link', { name: 'Anna Schmidt' }); await expect.element(link).toBeInTheDocument(); }); @@ -79,7 +79,7 @@ describe('DashboardMentions', () => { mentions: [makeMention({ documentId: undefined, actorName: 'Lena Bauer' })] }); await expect.element(page.getByText('Lena Bauer')).toBeInTheDocument(); - const links = page.getByRole('link'); - await expect.element(links).not.toBeInTheDocument(); + const actorLink = page.getByRole('link', { name: 'Lena Bauer' }); + await expect.element(actorLink).not.toBeInTheDocument(); }); }); diff --git a/frontend/src/lib/utils/date.ts b/frontend/src/lib/utils/date.ts index c2ab4e8e..a7122392 100644 --- a/frontend/src/lib/utils/date.ts +++ b/frontend/src/lib/utils/date.ts @@ -31,6 +31,63 @@ export function germanToIso(german: string): string { return `${y}-${m}-${d}`; } +/** + * Formats a raw date string into German DD.MM.YYYY format. + * + * Handles two modes: + * - Pure digit stream (no dots): auto-inserts dots after position 2 and 4 + * - Manual dot entry: preserves user-typed dots, pads single-digit day/month, + * and overflows extra digits from day→month and month→year + */ +export function formatGermanDateInput(raw: string): string { + if (!raw.includes('.')) { + const digits = raw.replace(/\D/g, '').slice(0, 8); + if (digits.length <= 2) return digits; + if (digits.length <= 4) return `${digits.slice(0, 2)}.${digits.slice(2)}`; + return `${digits.slice(0, 2)}.${digits.slice(2, 4)}.${digits.slice(4)}`; + } + + const trailingDot = raw.endsWith('.'); + const parts = raw.split('.').map((p) => p.replace(/\D/g, '')); + + let day = parts[0] ?? ''; + let month = parts[1] ?? ''; + let year = parts[2] ?? ''; + + let dayOverflowed = false; + if (day.length > 2) { + month = day.slice(2) + month; + day = day.slice(0, 2); + dayOverflowed = true; + } + + let monthOverflowed = false; + if (month.length > 2) { + year = month.slice(2) + year; + month = month.slice(0, 2); + monthOverflowed = true; + } + + year = year.slice(0, 4); + + const afterDay = !dayOverflowed && parts.length >= 2; + + if (day.length === 1 && (month || (trailingDot && !dayOverflowed))) { + day = '0' + day; + } + if (month.length === 1 && (year || (trailingDot && afterDay && !monthOverflowed))) { + month = '0' + month; + } + + if (year) return `${day}.${month}.${year}`; + if (month) { + const dot2 = trailingDot && afterDay && !monthOverflowed ? '.' : ''; + return `${day}.${month}${dot2}`; + } + const dot1 = trailingDot && !dayOverflowed ? '.' : ''; + return `${day}${dot1}`; +} + /** * Handles a date input event for German-format date fields (DD.MM.YYYY). * Strips non-digits, formats with dots, mutates the input's displayed value, @@ -38,15 +95,7 @@ export function germanToIso(german: string): string { */ export function handleGermanDateInput(e: Event): { display: string; iso: string } { const input = e.target as HTMLInputElement; - const digits = input.value.replace(/\D/g, '').slice(0, 8); - let display: string; - if (digits.length <= 2) { - display = digits; - } else if (digits.length <= 4) { - display = `${digits.slice(0, 2)}.${digits.slice(2)}`; - } else { - display = `${digits.slice(0, 2)}.${digits.slice(2, 4)}.${digits.slice(4)}`; - } + const display = formatGermanDateInput(input.value); input.value = display; return { display, iso: germanToIso(display) }; } diff --git a/frontend/src/routes/admin/groups/layout.svelte.spec.ts b/frontend/src/routes/admin/groups/layout.svelte.spec.ts index 05c48a81..eba52f63 100644 --- a/frontend/src/routes/admin/groups/layout.svelte.spec.ts +++ b/frontend/src/routes/admin/groups/layout.svelte.spec.ts @@ -109,9 +109,10 @@ describe('GroupsListPanel — collapse toggle', () => { }); it('persists collapse state using the groups-specific localStorage key', async () => { - const setSpy = vi.spyOn(Storage.prototype, 'setItem'); render(GroupsListPanel, { groups }); + const setSpy = vi.spyOn(Storage.prototype, 'setItem'); document.querySelector('[aria-label="Liste einklappen"]')!.click(); + await new Promise((r) => setTimeout(r, 0)); expect(setSpy).toHaveBeenCalledWith('admin_groups_list_collapsed', 'true'); setSpy.mockRestore(); }); diff --git a/frontend/src/routes/admin/tags/layout.svelte.spec.ts b/frontend/src/routes/admin/tags/layout.svelte.spec.ts index 8bf7eb61..40aab79f 100644 --- a/frontend/src/routes/admin/tags/layout.svelte.spec.ts +++ b/frontend/src/routes/admin/tags/layout.svelte.spec.ts @@ -88,9 +88,10 @@ describe('TagsListPanel — collapse toggle', () => { }); it('persists collapse state using the tags-specific localStorage key', async () => { - const setSpy = vi.spyOn(Storage.prototype, 'setItem'); render(TagsListPanel, { tags }); + const setSpy = vi.spyOn(Storage.prototype, 'setItem'); document.querySelector('[aria-label="Liste einklappen"]')!.click(); + await new Promise((r) => setTimeout(r, 0)); expect(setSpy).toHaveBeenCalledWith('admin_tags_list_collapsed', 'true'); setSpy.mockRestore(); }); diff --git a/frontend/src/routes/admin/users/layout.svelte.spec.ts b/frontend/src/routes/admin/users/layout.svelte.spec.ts index 865ddbe6..d10a3e0b 100644 --- a/frontend/src/routes/admin/users/layout.svelte.spec.ts +++ b/frontend/src/routes/admin/users/layout.svelte.spec.ts @@ -131,9 +131,10 @@ describe('UsersListPanel — collapse toggle', () => { }); it('persists collapse state using the users-specific localStorage key', async () => { - const setSpy = vi.spyOn(Storage.prototype, 'setItem'); render(UsersListPanel, { users }); + const setSpy = vi.spyOn(Storage.prototype, 'setItem'); document.querySelector('[aria-label="Liste einklappen"]')!.click(); + await new Promise((r) => setTimeout(r, 0)); expect(setSpy).toHaveBeenCalledWith('admin_users_list_collapsed', 'true'); setSpy.mockRestore(); }); diff --git a/frontend/src/routes/conversations/page.svelte.spec.ts b/frontend/src/routes/conversations/page.svelte.spec.ts index 85f18e63..ebc7c4b3 100644 --- a/frontend/src/routes/conversations/page.svelte.spec.ts +++ b/frontend/src/routes/conversations/page.svelte.spec.ts @@ -48,9 +48,9 @@ const withDocs = { // ─── Empty state ────────────────────────────────────────────────────────────── describe('Conversations page – empty state', () => { - it('shows the "select two persons" prompt when no persons are selected', async () => { + it('shows the empty-state heading when no persons are selected', async () => { render(Page, { data: baseData }); - await expect.element(page.getByText(/Wählen Sie zwei Personen aus/i)).toBeInTheDocument(); + await expect.element(page.getByText(/Korrespondenz durchsuchen/i)).toBeInTheDocument(); }); it('hides the swap button when no persons are selected', async () => { diff --git a/frontend/src/routes/login/page.svelte.spec.ts b/frontend/src/routes/login/page.svelte.spec.ts index 02b4d5e0..6cf420a8 100644 --- a/frontend/src/routes/login/page.svelte.spec.ts +++ b/frontend/src/routes/login/page.svelte.spec.ts @@ -10,7 +10,9 @@ afterEach(cleanup); describe('Login page – rendering', () => { it('renders the page title', async () => { render(LoginPage, {}); - await expect.element(page.getByRole('link', { name: 'Familienarchiv' })).toBeInTheDocument(); + await expect + .element(page.getByRole('link', { name: 'Familienarchiv' }).first()) + .toBeInTheDocument(); await page.screenshot({ path: 'test-results/screenshots/login-default.png' }); }); diff --git a/frontend/src/routes/persons/page.svelte.spec.ts b/frontend/src/routes/persons/page.svelte.spec.ts index 9fbb8f7f..113df55d 100644 --- a/frontend/src/routes/persons/page.svelte.spec.ts +++ b/frontend/src/routes/persons/page.svelte.spec.ts @@ -64,7 +64,7 @@ describe('Persons page – rendering', () => { it('shows alias in italic when provided', async () => { render(Page, { data: { ...emptyData, persons: [makePerson({ alias: 'Maxi' })] } }); - await expect.element(page.getByText('"Maxi"')).toBeInTheDocument(); + await expect.element(page.getByText('„Maxi"')).toBeInTheDocument(); }); it('shows life date range when birthYear is provided', async () => {