Aufraeumarbeiten: ESLint, DOM-Verschachtelung, eigene Testwehr

ESLint hatte keine Konfiguration — das Skript "npm run lint" pruefte nichts.
Jetzt Flat Config ohne Formatierungsregeln, dafuer mit den Regeln, die die
Fehler dieses Projekts gemeldet haetten. Er hat gleich zwei gefunden, die
niemand gesucht hatte.

Das Badge rendert ein span statt eines div. Damit ist die Meldung
"div cannot be a descendant of p" weg, die seit Phase 3 auf der Einsatzseite
stand — sie ist der klassische Ausloeser fuer einen Hydration-Fehler.

Die E2E-Tests laufen gegen eine eigene Wehr. Nach fuenf Phasen standen in der
Beispielwehr 120 Atemschutz-Ueberwachungen statt einer und 115 Geraete statt
45; bei jedem Lauf aenderten sich alle Bildschirmfotos. Der Bestand ist einmal
zurueckgesetzt, npm run db:test-reset baut die Testwehr neu auf.

Behoben nebenbei:
- Die Fehlerflaeche der Karte konnte nie etwas anzeigen.
- Zwei useMemo der Lagekarte rechneten bei jedem Rendern neu.
- tests/e2e war vom Typecheck ausgenommen und liess einen doppelt
  deklarierten Bezeichner durch.

Closes #10
Closes #11
Closes #12

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-04 13:24:48 +02:00
co-authored by Claude Opus 5
parent 8081131eb7
commit 7196c7110c
94 changed files with 1447 additions and 428 deletions
+81 -44
View File
@@ -1,4 +1,5 @@
import { expect, test, type Page } from "@playwright/test";
import { expect, test } from "@playwright/test";
import { anmelden, eigenerName } from "./wehr";
/**
* E2E-Tests für Phase 8 — Wissen, Belege und die KI-Protokolle.
@@ -20,32 +21,23 @@ import { expect, test, type Page } from "@playwright/test";
*/
test.describe.configure({ mode: "serial" });
const ADMIN_EMAIL = process.env.SEED_ADMIN_EMAIL ?? "admin@beispielwehr.de";
const ADMIN_PASSWORT = process.env.SEED_ADMIN_PASSWORT ?? "Feuerwehr2026";
function eigenerTitel(zweck: string): string {
return `E2E-${zweck}-${Date.now().toString(36)}`;
}
async function anmelden(page: Page): Promise<void> {
await page.goto("/anmelden");
await page.getByLabel("E-Mail-Adresse").fill(ADMIN_EMAIL);
await page.getByLabel("Passwort").fill(ADMIN_PASSWORT);
await page.getByRole("button", { name: "Anmelden" }).click();
await page.waitForURL("/");
}
test.describe("Wissen", () => {
test("zeigt die Seiten aus dem Bestand", async ({ page }) => {
await anmelden(page);
await page.goto("/wiki");
await expect(page.getByRole("heading", { name: "Wissen", level: 1 })).toBeVisible();
await expect(
page.getByRole("heading", { name: "Wissen", level: 1 }),
).toBeVisible();
// Aus dem Seed.
await expect(page.getByRole("link", { name: "Prüffristen im Überblick" })).toBeVisible();
await expect(
page.getByRole("link", { name: "Atemschutz — was vor jedem Einsatz gilt" }),
page.getByRole("link", { name: "Prüffristen im Überblick" }),
).toBeVisible();
await expect(
page.getByRole("link", {
name: "Atemschutz — was vor jedem Einsatz gilt",
}),
).toBeVisible();
});
@@ -53,11 +45,15 @@ test.describe("Wissen", () => {
* Der wichtigste Test der Datei: ohne Modell muss dastehen, dass keines da
* ist — und im selben Atemzug, dass die Wortsuche trotzdem funktioniert.
*/
test("sagt ohne Sprachmodell, was fehlt und was trotzdem geht", async ({ page }) => {
test("sagt ohne Sprachmodell, was fehlt und was trotzdem geht", async ({
page,
}) => {
await anmelden(page);
await page.goto("/wiki");
await expect(page.getByText("Es ist kein Sprachmodell eingerichtet.")).toBeVisible();
await expect(
page.getByText("Es ist kein Sprachmodell eingerichtet."),
).toBeVisible();
await expect(page.getByText(/Wortsuche/)).toBeVisible();
});
@@ -72,7 +68,9 @@ test.describe("Wissen", () => {
// Der Ausschnitt zeigt die Textstelle mitsamt ihrer Umgebung, nicht bloß
// den Seitennamen — daran erkennt man beim Suchen, ob es die richtige
// Stelle ist, ohne die Seite zu öffnen.
await expect(page.getByText(/Der Rückzugsdruck ist der Druck/).first()).toBeVisible();
await expect(
page.getByText(/Der Rückzugsdruck ist der Druck/).first(),
).toBeVisible();
});
test("zeigt eine Seite als formatierten Text", async ({ page }) => {
@@ -80,11 +78,16 @@ test.describe("Wissen", () => {
await page.goto("/wiki/atemschutz-merkblatt");
await expect(
page.getByRole("heading", { name: "Atemschutz — was vor jedem Einsatz gilt", level: 1 }),
page.getByRole("heading", {
name: "Atemschutz — was vor jedem Einsatz gilt",
level: 1,
}),
).toBeVisible();
// Markdown wird als echte Überschrift dargestellt, nicht als Raute im Text.
await expect(page.getByRole("heading", { name: "Rückzugsdruck" })).toBeVisible();
await expect(
page.getByRole("heading", { name: "Rückzugsdruck" }),
).toBeVisible();
await expect(page.getByText(/FwDV 7, Ausgabe 2002/)).toBeVisible();
});
@@ -92,7 +95,9 @@ test.describe("Wissen", () => {
await anmelden(page);
await page.goto("/wiki/schlaeuche-pflegen");
await expect(page.getByRole("columnheader", { name: "Prüfdruck" })).toBeVisible();
await expect(
page.getByRole("columnheader", { name: "Prüfdruck" }),
).toBeVisible();
await expect(page.getByRole("cell", { name: "C 42 / C 52" })).toBeVisible();
});
@@ -104,22 +109,30 @@ test.describe("Wissen", () => {
await anmelden(page);
await page.goto("/wiki/alarmierungsordnung");
await expect(page.getByText(/Suchstand ist älter als der Text/)).toBeVisible();
await expect(
page.getByText(/Suchstand ist älter als der Text/),
).toBeVisible();
});
test("legt eine Seite an und zeigt sie sofort", async ({ page }) => {
const titel = eigenerTitel("Merkblatt");
const titel = eigenerName("Merkblatt");
await anmelden(page);
await page.goto("/wiki");
await page.getByRole("button", { name: "Seite anlegen" }).click();
await page.getByLabel("Titel").fill(titel);
await page.getByLabel("Inhalt").fill("## Erster Abschnitt\n\nHier steht der Text.");
await page
.getByLabel("Inhalt")
.fill("## Erster Abschnitt\n\nHier steht der Text.");
await page.getByRole("button", { name: "Anlegen" }).click();
await expect(page.getByRole("heading", { name: titel, level: 1 })).toBeVisible();
await expect(page.getByRole("heading", { name: "Erster Abschnitt" })).toBeVisible();
await expect(
page.getByRole("heading", { name: titel, level: 1 }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Erster Abschnitt" }),
).toBeVisible();
});
/**
@@ -127,7 +140,7 @@ test.describe("Wissen", () => {
* die sich unbemerkt ändert, ist schlimmer als keine.
*/
test("hebt die vorherige Fassung auf", async ({ page }) => {
const titel = eigenerTitel("Fassungen");
const titel = eigenerName("Fassungen");
await anmelden(page);
await page.goto("/wiki");
@@ -136,12 +149,16 @@ test.describe("Wissen", () => {
await page.getByLabel("Titel").fill(titel);
await page.getByLabel("Inhalt").fill("Der erste Text.");
await page.getByRole("button", { name: "Anlegen" }).click();
await expect(page.getByRole("heading", { name: titel, level: 1 })).toBeVisible();
await expect(
page.getByRole("heading", { name: titel, level: 1 }),
).toBeVisible();
await page.getByRole("button", { name: "Bearbeiten" }).click();
const dialog = page.getByRole("dialog");
await dialog.getByLabel("Inhalt").fill("Der zweite Text.");
await dialog.getByLabel("Was wurde geändert? (freiwillig)").fill("Text berichtigt");
await dialog
.getByLabel("Was wurde geändert? (freiwillig)")
.fill("Text berichtigt");
await dialog.getByRole("button", { name: "Speichern" }).click();
await expect(page.getByText("Der zweite Text.")).toBeVisible();
@@ -154,16 +171,20 @@ test.describe("Wissen", () => {
await expect(verlauf.getByText("Der erste Text.")).toBeVisible();
});
test("zeigt beim Bearbeiten, in wie viele Abschnitte zerlegt wird", async ({ page }) => {
test("zeigt beim Bearbeiten, in wie viele Abschnitte zerlegt wird", async ({
page,
}) => {
await anmelden(page);
await page.goto("/wiki/pruefristen-im-ueberblick");
await page.getByRole("button", { name: "Bearbeiten" }).click();
await expect(page.getByText(/Wird für die Suche in \d+ Abschnitte zerlegt/)).toBeVisible();
await expect(
page.getByText(/Wird für die Suche in \d+ Abschnitte zerlegt/),
).toBeVisible();
});
test("ein Entwurf wird als solcher gekennzeichnet", async ({ page }) => {
const titel = eigenerTitel("Entwurf");
const titel = eigenerName("Entwurf");
await anmelden(page);
await page.goto("/wiki");
@@ -182,14 +203,22 @@ test.describe("Wissen", () => {
});
test.describe("Belege", () => {
test("sagt ohne Vision-Modell, was fehlt und was trotzdem geht", async ({ page }) => {
test("sagt ohne Vision-Modell, was fehlt und was trotzdem geht", async ({
page,
}) => {
await anmelden(page);
await page.goto("/belege");
await expect(page.getByRole("heading", { name: "Belege", level: 1 })).toBeVisible();
await expect(page.getByText(/kein Modell eingerichtet, das Bilder lesen kann/)).toBeVisible();
await expect(
page.getByRole("heading", { name: "Belege", level: 1 }),
).toBeVisible();
await expect(
page.getByText(/kein Modell eingerichtet, das Bilder lesen kann/),
).toBeVisible();
await expect(page.getByText(/von Hand eingetragen/)).toBeVisible();
await expect(page.getByRole("button", { name: "Beleg fotografieren" })).toBeVisible();
await expect(
page.getByRole("button", { name: "Beleg fotografieren" }),
).toBeVisible();
});
});
@@ -198,19 +227,27 @@ test.describe("Protokolle", () => {
await anmelden(page);
await page.goto("/ki-protokoll");
await expect(page.getByRole("heading", { name: "KI-Protokoll", level: 1 })).toBeVisible();
await expect(
page.getByRole("heading", { name: "KI-Protokoll", level: 1 }),
).toBeVisible();
await expect(page.getByText("Vorgänge insgesamt")).toBeVisible();
await expect(page.getByText("erzeugt, aber nicht entschieden")).toBeVisible();
await expect(
page.getByText("erzeugt, aber nicht entschieden"),
).toBeVisible();
// Die Zusicherung, die den Zweck der Liste ausmacht.
await expect(page.getByText(/lassen sich nachträglich nicht ändern/)).toBeVisible();
await expect(
page.getByText(/lassen sich nachträglich nicht ändern/),
).toBeVisible();
});
test("zeigt das Audit-Log", async ({ page }) => {
await anmelden(page);
await page.goto("/protokoll");
await expect(page.getByRole("heading", { name: "Protokoll", level: 1 })).toBeVisible();
await expect(
page.getByRole("heading", { name: "Protokoll", level: 1 }),
).toBeVisible();
await expect(page.getByText(/append-only/)).toBeVisible();
// Die Anmeldung von eben steht schon drin.