test(coverage): drive browser tests to 80% on all metrics (#496) #505

Merged
marcel merged 189 commits from feat/issue-496-browser-coverage-tests into main 2026-05-11 21:50:39 +02:00
3 changed files with 248 additions and 0 deletions
Showing only changes of commit 151d7e7a29 - Show all commits

View File

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

View File

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

View File

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