test: add e2e tests
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
||||
import PersonMultiSelect from '$lib/components/PersonMultiSelect.svelte';
|
||||
import { untrack } from 'svelte';
|
||||
import { isoToGerman, germanToIso } from '$lib/utils';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
@@ -12,19 +13,6 @@
|
||||
let senderId = $state(doc.sender?.id ?? '');
|
||||
let selectedReceivers = $state(doc.receivers ?? []);
|
||||
|
||||
function isoToGerman(iso: string): string {
|
||||
if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return '';
|
||||
const [y, m, d] = iso.split('-');
|
||||
return `${d}.${m}.${y}`;
|
||||
}
|
||||
|
||||
function germanToIso(german: string): string {
|
||||
const match = german.match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
||||
if (!match) return '';
|
||||
const [, d, m, y] = match;
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
let dateDisplay = $state(isoToGerman(doc.documentDate ?? ''));
|
||||
let dateIso = $state(doc.documentDate ?? '');
|
||||
let dateDirty = $state(false);
|
||||
|
||||
87
frontend/src/routes/login/page.svelte.spec.ts
Normal file
87
frontend/src/routes/login/page.svelte.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import LoginPage from './+page.svelte';
|
||||
|
||||
const tick = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
describe('Login page – rendering', () => {
|
||||
it('renders the page title', async () => {
|
||||
render(LoginPage, {});
|
||||
await expect.element(page.getByText('Familienarchiv')).toBeInTheDocument();
|
||||
await page.screenshot({ path: 'test-results/screenshots/login-default.png' });
|
||||
});
|
||||
|
||||
it('renders the submit button', async () => {
|
||||
render(LoginPage, {});
|
||||
await expect.element(page.getByRole('button', { name: 'Anmelden' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the username input', async () => {
|
||||
render(LoginPage, {});
|
||||
await tick();
|
||||
const input = document.querySelector<HTMLInputElement>('input[name="username"]');
|
||||
expect(input).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders the password input', async () => {
|
||||
render(LoginPage, {});
|
||||
await tick();
|
||||
const input = document.querySelector<HTMLInputElement>('input[name="password"]');
|
||||
expect(input).not.toBeNull();
|
||||
});
|
||||
|
||||
it('username field is required', async () => {
|
||||
render(LoginPage, {});
|
||||
await tick();
|
||||
const input = document.querySelector<HTMLInputElement>('input[name="username"]');
|
||||
expect(input?.required).toBe(true);
|
||||
});
|
||||
|
||||
it('password field is required', async () => {
|
||||
render(LoginPage, {});
|
||||
await tick();
|
||||
const input = document.querySelector<HTMLInputElement>('input[name="password"]');
|
||||
expect(input?.required).toBe(true);
|
||||
});
|
||||
|
||||
it('password field has type="password"', async () => {
|
||||
render(LoginPage, {});
|
||||
await tick();
|
||||
const input = document.querySelector<HTMLInputElement>('input[name="password"]');
|
||||
expect(input?.type).toBe('password');
|
||||
});
|
||||
|
||||
it('form submits to the login action', async () => {
|
||||
render(LoginPage, {});
|
||||
await tick();
|
||||
const form = document.querySelector<HTMLFormElement>('form');
|
||||
expect(form?.action).toMatch(/\?\/login$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Login page – error state', () => {
|
||||
it('shows no error when form is undefined', async () => {
|
||||
render(LoginPage, {});
|
||||
await tick();
|
||||
expect(document.querySelector('.text-red-600')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows no error when form has no error property', async () => {
|
||||
render(LoginPage, { form: {} });
|
||||
await tick();
|
||||
expect(document.querySelector('.text-red-600')).toBeNull();
|
||||
});
|
||||
|
||||
it('displays the error message from the form action', async () => {
|
||||
render(LoginPage, { form: { error: 'Ungültige Anmeldedaten.' } });
|
||||
await expect.element(page.getByText('Ungültige Anmeldedaten.')).toBeInTheDocument();
|
||||
await page.screenshot({ path: 'test-results/screenshots/login-error.png' });
|
||||
});
|
||||
|
||||
it('applies red styling to the error text', async () => {
|
||||
render(LoginPage, { form: { error: 'Fehler!' } });
|
||||
await tick();
|
||||
expect(document.querySelector('.text-red-600')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,172 @@
|
||||
import { page } from 'vitest/browser';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import Page from './+page.svelte';
|
||||
|
||||
describe('/+page.svelte', () => {
|
||||
it('should render h1', async () => {
|
||||
render(Page);
|
||||
|
||||
const heading = page.getByRole('heading', { level: 1 });
|
||||
await expect.element(heading).toBeInTheDocument();
|
||||
const tick = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
||||
|
||||
// Silence fetch calls from PersonTypeahead when advanced filters are open
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) })
|
||||
);
|
||||
|
||||
// ─── Test data ────────────────────────────────────────────────────────────────
|
||||
|
||||
const emptyData = {
|
||||
filters: { q: '', from: '', to: '', senderId: '', receiverId: '', tags: [] },
|
||||
documents: [],
|
||||
initialValues: { senderName: '', receiverName: '' },
|
||||
error: null
|
||||
};
|
||||
|
||||
const makeDoc = (overrides = {}) => ({
|
||||
id: '1',
|
||||
title: 'Testbrief',
|
||||
originalFilename: 'testbrief.pdf',
|
||||
status: 'UPLOADED',
|
||||
documentDate: '2024-03-15',
|
||||
location: 'Berlin',
|
||||
sender: { id: 'p1', firstName: 'Max', lastName: 'Mustermann' },
|
||||
receivers: [{ id: 'p2', firstName: 'Anna', lastName: 'Musterfrau' }],
|
||||
tags: [{ name: 'Familie' }],
|
||||
filePath: '/files/testbrief.pdf',
|
||||
...overrides
|
||||
});
|
||||
|
||||
const dataWithDocs = { ...emptyData, documents: [makeDoc()] };
|
||||
|
||||
// ─── Search bar ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Home page – search bar', () => {
|
||||
it('renders the full-text search input', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
await expect
|
||||
.element(page.getByPlaceholder('Suche in Titel, Inhalt, Ort...'))
|
||||
.toBeInTheDocument();
|
||||
await page.screenshot({ path: 'test-results/screenshots/home-default.png' });
|
||||
});
|
||||
|
||||
it('renders the filter toggle button', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
// Use exact match to avoid collision with the empty-state "Alle Filter löschen" button
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'Filter', exact: true }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the reset link pointing to /', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
const resetLink = page.getByTitle('Filter zurücksetzen');
|
||||
await expect.element(resetLink).toBeInTheDocument();
|
||||
await expect.element(resetLink).toHaveAttribute('href', '/');
|
||||
});
|
||||
|
||||
it('pre-fills the search input from filters.q', async () => {
|
||||
render(Page, { data: { ...emptyData, filters: { ...emptyData.filters, q: 'Urlaub' } } });
|
||||
await expect.element(page.getByPlaceholder('Suche in Titel, Inhalt, Ort...')).toHaveValue('Urlaub');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Advanced filters ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('Home page – advanced filters', () => {
|
||||
it('hides the advanced filters by default', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
// Date inputs are inside the {#if showAdvanced} block → not in DOM
|
||||
await tick();
|
||||
expect(document.querySelector('input[id="from"]')).toBeNull();
|
||||
expect(document.querySelector('input[id="to"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('toggles the advanced filter panel open on button click', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
await page.getByRole('button', { name: 'Filter', exact: true }).click();
|
||||
await tick();
|
||||
expect(document.querySelector('input[id="from"]')).not.toBeNull();
|
||||
expect(document.querySelector('input[id="to"]')).not.toBeNull();
|
||||
await page.screenshot({ path: 'test-results/screenshots/home-filters-open.png' });
|
||||
});
|
||||
|
||||
it('collapses the advanced filter panel on second click', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
const btn = page.getByRole('button', { name: 'Filter', exact: true });
|
||||
await btn.click();
|
||||
// Wait for the input to appear before clicking again
|
||||
await expect.element(page.getByText('Schlagworte')).toBeInTheDocument();
|
||||
await btn.click();
|
||||
// Wait for slide transition to finish
|
||||
await expect.element(page.getByText('Schlagworte')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the tag filter section when filters are open', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
await page.getByRole('button', { name: 'Filter', exact: true }).click();
|
||||
await expect.element(page.getByText('Schlagworte')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Document list ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Home page – document list', () => {
|
||||
it('shows empty state when there are no documents', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
await expect.element(page.getByText('Keine Dokumente gefunden')).toBeInTheDocument();
|
||||
await page.screenshot({ path: 'test-results/screenshots/home-empty-state.png' });
|
||||
});
|
||||
|
||||
it('renders a document with title, date, location, sender and receiver', async () => {
|
||||
render(Page, { data: dataWithDocs });
|
||||
await expect.element(page.getByText('Testbrief')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('15. März 2024')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Berlin')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Anna Musterfrau')).toBeInTheDocument();
|
||||
await page.screenshot({ path: 'test-results/screenshots/home-with-documents.png' });
|
||||
});
|
||||
|
||||
it('renders a tag chip for each document tag', async () => {
|
||||
render(Page, { data: dataWithDocs });
|
||||
await expect.element(page.getByRole('button', { name: 'Familie' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "Unbekannt" for sender when sender is null', async () => {
|
||||
const data = { ...emptyData, documents: [makeDoc({ sender: null })] };
|
||||
render(Page, { data });
|
||||
await expect.element(page.getByText('Unbekannt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders original filename when title is empty', async () => {
|
||||
const data = { ...emptyData, documents: [makeDoc({ title: null })] };
|
||||
render(Page, { data });
|
||||
await expect.element(page.getByText('testbrief.pdf')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('links each document to its detail page', async () => {
|
||||
render(Page, { data: dataWithDocs });
|
||||
const link = page.getByRole('link', { name: /Testbrief/ });
|
||||
await expect.element(link).toHaveAttribute('href', '/documents/1');
|
||||
});
|
||||
|
||||
it('renders the "Neues Dokument" link', async () => {
|
||||
render(Page, { data: emptyData });
|
||||
const link = page.getByRole('link', { name: /Neues Dokument/i });
|
||||
await expect.element(link).toBeInTheDocument();
|
||||
await expect.element(link).toHaveAttribute('href', '/documents/new');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Error state ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Home page – error state', () => {
|
||||
it('shows the error message when data.error is set', async () => {
|
||||
const data = { ...emptyData, error: 'Daten konnten nicht geladen werden.' };
|
||||
render(Page, { data });
|
||||
await expect
|
||||
.element(page.getByText('Daten konnten nicht geladen werden.'))
|
||||
.toBeInTheDocument();
|
||||
await page.screenshot({ path: 'test-results/screenshots/home-error.png' });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user