test: unit-tests der kernlogik und playwright-smoke-test

50 Unit-Tests mit Vitest fuer die Stellen, an denen ein Fehler teuer
waere: Besitzstatus, Mehrfachauswahl inklusive Ueberspringen von
Dubletten und bereits besessenen Sets, Token-Erzeugung und -Vergleich,
Cent-Umrechnung und Slugs.

Der Playwright-Smoke-Test laeuft den ganzen Weg: Registrierung des
ersten Nutzers, Hersteller anlegen, Set anlegen, auf eine neue
Wunschliste setzen, Link holen und als Besucher ohne Konto reservieren.
Er nutzt ein eigenes Datenverzeichnis (./data-e2e), das vor jedem Lauf
geleert wird.

Dabei aufgefallen und behoben: eine spontan aus dem Katalog angelegte
Wunschliste war nicht teilbar, weil das Feld isPublic ohne Formular
auf false fiel. Ausserdem ist das Sternchen an Pflichtfeldern jetzt
aria-hidden, damit der zugaengliche Name des Feldes sauber bleibt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
unknown
2026-09-06 23:31:21 +02:00
co-authored by Claude Opus 5
parent 79b0133083
commit b0152b8667
16 changed files with 1289 additions and 746 deletions
+13
View File
@@ -0,0 +1,13 @@
import { execFileSync } from 'node:child_process';
/**
* Migrates the smoke-test database. The directory itself is wiped earlier by
* scripts/reset-e2e.mjs, because Playwright starts its web server before this
* hook runs and the open database file would block the delete on Windows.
*/
export default function globalSetup() {
execFileSync('node', ['node_modules/tsx/dist/cli.mjs', 'scripts/migrate.ts'], {
stdio: 'inherit',
env: { ...process.env, DATA_DIR: './data-e2e' },
});
}
+92
View File
@@ -0,0 +1,92 @@
import { expect, test } from '@playwright/test';
/**
* The one path that has to work: register, put a set into the catalog, and
* share it on a wishlist that a visitor can reserve from without an account.
*/
test('Registrieren, Set anlegen, Wunschliste teilen und reservieren', async ({ page, browser }) => {
// --- first run: the very first visitor registers and becomes admin --------
await page.goto('/');
await expect(page).toHaveURL(/\/registrieren/);
// Card titles are plain elements, not headings - hence getByText here.
await expect(page.getByText('Erstes Konto anlegen')).toBeVisible();
await page.getByLabel('Name').fill('Florian');
await page.getByLabel('E-Mail').fill('test@bricktrack.local');
await page.getByLabel('Passwort').fill('smoketest-passwort');
await page.getByRole('button', { name: 'Konto anlegen' }).click();
await expect(page.getByRole('heading', { name: 'Hallo Florian' })).toBeVisible();
// --- master data: a set needs a manufacturer -----------------------------
await page.goto('/verwaltung/hersteller');
await page.getByRole('button', { name: 'Hersteller anlegen' }).click();
await page.getByLabel('Name').fill('Lego');
await page.getByRole('button', { name: 'Speichern' }).click();
await expect(page.getByText('Lego')).toBeVisible();
// --- catalog: create a set ------------------------------------------------
await page.goto('/katalog/neu');
await page.getByLabel('Setnummer').fill('60398');
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Familienhaus und Elektroauto');
await page.getByLabel('Jahr').fill('2023');
await page.getByLabel('UVP').fill('49,99');
await page.getByRole('button', { name: 'Set anlegen' }).click();
await expect(page.getByRole('heading', { name: 'Familienhaus und Elektroauto' })).toBeVisible();
await expect(page.getByText('Fehlt noch')).toBeVisible();
// --- wishlist: put the set on a fresh list --------------------------------
await page.getByRole('button', { name: 'Auf Wunschliste' }).click();
await page.getByRole('combobox', { name: 'Wunschliste' }).selectOption('__neu__');
await page.getByLabel('Titel der neuen Liste').fill('Weihnachten 2026');
await page.getByRole('button', { name: 'Hinzufügen' }).click();
// Wait for the confirmation, not for the button label that is on screen anyway.
await expect(page.getByText('Auf die Wunschliste gesetzt.')).toBeVisible();
// --- sharing: grab the public link ---------------------------------------
await page.goto('/wunschlisten');
// Navigate by href: a toast can still be on screen and would swallow the click.
const listHref = await page
.getByRole('link', { name: /Weihnachten 2026/ })
.getAttribute('href');
expect(listHref).toMatch(/^\/wunschlisten\//);
await page.goto(listHref!);
await expect(page.getByRole('heading', { name: 'Weihnachten 2026' })).toBeVisible();
const shareUrl = await page.getByLabel('Link zum Teilen').inputValue();
expect(shareUrl).toContain('/w/');
// --- the visitor: no account, no session ---------------------------------
const visitorContext = await browser.newContext();
const visitor = await visitorContext.newPage();
await visitor.goto(shareUrl);
await expect(visitor.getByRole('heading', { name: 'Weihnachten 2026' })).toBeVisible();
await expect(visitor.getByText('Familienhaus und Elektroauto')).toBeVisible();
await visitor.getByRole('button', { name: 'Das schenke ich' }).click();
await visitor.getByLabel('Dein Name').fill('Tante Anne');
await visitor.getByRole('button', { name: 'Reservieren' }).click();
await expect(visitor.getByText('Reservierung gespeichert')).toBeVisible();
await visitor.getByRole('button', { name: 'Alles klar' }).click();
await expect(visitor.getByText('Das schenkst du')).toBeVisible();
// --- back on the owner side: the reservation shows up ---------------------
await page.reload();
await expect(page.getByText('Reserviert von Tante Anne')).toBeVisible();
await visitorContext.close();
});
test('Ohne Anmeldung fuehrt jeder geschuetzte Pfad zur Anmeldung', async ({ page }) => {
await page.goto('/sammlung');
await expect(page).toHaveURL(/\/anmelden|\/registrieren/);
});
test('Ein unbekannter Share-Token liefert 404', async ({ page }) => {
const response = await page.goto('/w/dieser-token-existiert-nicht');
expect(response?.status()).toBe(404);
});