fix(tests): fix 27 failing frontend unit tests

Six categories of breakage:

1. date.ts — add formatGermanDateInput(raw: string): string as a pure
   function covering both digit-stream auto-dot and manual-dot-with-padding
   modes. Refactor handleGermanDateInput to delegate to it. Fixes 16 failures
   in date.spec.ts where the function was imported but didn't exist.

2. Admin layout specs (groups/tags/users) — $effect fires on initial mount
   with manualCollapse=false, so the spy captured 'false' before the click's
   effect ran. Fix: move spy setup after render(), add await setTimeout(0) to
   flush Svelte effects before asserting.

3. DashboardMentions — component now renders a persistent
   "Benachrichtigungsverlauf ansehen" link, making getByRole('link') strict-
   mode violations. Fix: scope link queries to the actor name, and check
   absence of the actor link (not all links) in the no-documentId test.

4. Conversations page — empty-state copy changed from "Wählen Sie zwei
   Personen aus" to "Korrespondenz durchsuchen". Update the test.

5. Login page — AuthHeader adds a second aria-label="Familienarchiv" link.
   Use .first() to avoid strict-mode violation.

6. Persons page — alias is rendered with German quotation marks „…" not
   straight quotes "…". Update the test string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-31 17:28:35 +02:00
parent fb636e4152
commit 6d61297182
8 changed files with 75 additions and 21 deletions

View File

@@ -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();
});
});

View File

@@ -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) };
}

View File

@@ -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<HTMLButtonElement>('[aria-label="Liste einklappen"]')!.click();
await new Promise((r) => setTimeout(r, 0));
expect(setSpy).toHaveBeenCalledWith('admin_groups_list_collapsed', 'true');
setSpy.mockRestore();
});

View File

@@ -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<HTMLButtonElement>('[aria-label="Liste einklappen"]')!.click();
await new Promise((r) => setTimeout(r, 0));
expect(setSpy).toHaveBeenCalledWith('admin_tags_list_collapsed', 'true');
setSpy.mockRestore();
});

View File

@@ -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<HTMLButtonElement>('[aria-label="Liste einklappen"]')!.click();
await new Promise((r) => setTimeout(r, 0));
expect(setSpy).toHaveBeenCalledWith('admin_users_list_collapsed', 'true');
setSpy.mockRestore();
});

View File

@@ -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 () => {

View File

@@ -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' });
});

View File

@@ -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 () => {