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
+76 -37
View File
@@ -1,4 +1,5 @@
import { expect, test, type Page } from "@playwright/test";
import { expect, test } from "@playwright/test";
import { anmelden } from "./wehr";
/**
* E2E-Tests für Phase 3.
@@ -14,17 +15,6 @@ import { expect, test, type Page } from "@playwright/test";
* Voraussetzung: Stack läuft, Datenbank ist migriert und geseedet.
*/
const ADMIN_EMAIL = process.env.SEED_ADMIN_EMAIL ?? "admin@beispielwehr.de";
const ADMIN_PASSWORT = process.env.SEED_ADMIN_PASSWORT ?? "Feuerwehr2026";
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.beforeEach(async ({ page }) => {
await anmelden(page);
});
@@ -35,10 +25,17 @@ test.describe("Einsatzliste", () => {
await expect(page.getByRole("heading", { name: "Einsätze" })).toBeVisible();
// Aus dem Seed: acht Einsätze des laufenden Jahres.
await expect(page.getByRole("link", { name: /\d{4}-\d{4}/ }).first()).toBeVisible();
// "exact": in der Liste stehen mit der Zeit auch Einsaetze, deren Stichwort
// mit demselben Text beginnt — etwa die aus den Tests weiter unten.
await expect(page.getByText("B 3 Wohnhausbrand", { exact: true })).toBeVisible();
await expect(
page.getByRole("link", { name: /\d{4}-\d{4}/ }).first(),
).toBeVisible();
// Die Zelle enthaelt Stichwort UND Objekt ("B 3 Wohnhausbrand" +
// "Reihenhaus, zwei Vollgeschosse"). Deshalb der Anker am Anfang statt
// einer Pruefung auf Gleichheit — die ging bis Phase 8 nur deshalb durch,
// weil in der Beispielwehr Einsaetze aus frueheren Testlaeufen ohne Objekt
// standen.
await expect(
page.getByRole("cell", { name: /^B 3 Wohnhausbrand/ }).first(),
).toBeVisible();
});
test("filtert über die Suche", async ({ page }) => {
@@ -76,7 +73,9 @@ test.describe("Einsatz erfassen und bearbeiten", () => {
await page.waitForURL(/\/einsaetze\/[0-9a-f-]{36}$/);
einsatzAdresse = page.url();
await expect(page.getByRole("heading", { name: /THL 1 Testeinsatz/ })).toBeVisible();
await expect(
page.getByRole("heading", { name: /THL 1 Testeinsatz/ }),
).toBeVisible();
await expect(page.getByText("läuft", { exact: true })).toBeVisible();
});
@@ -86,20 +85,28 @@ test.describe("Einsatz erfassen und bearbeiten", () => {
await expect(page.getByRole("heading", { name: "Zeiten" })).toBeVisible();
await page.getByRole("button", { name: "ausgerückt jetzt" }).click();
await expect(page.getByRole("button", { name: "ausgerückt zurücknehmen" })).toBeVisible();
await expect(
page.getByRole("button", { name: "ausgerückt zurücknehmen" }),
).toBeVisible();
await page.getByRole("button", { name: "eingetroffen jetzt" }).click();
await expect(page.getByRole("button", { name: "eingetroffen zurücknehmen" })).toBeVisible();
await expect(
page.getByRole("button", { name: "eingetroffen zurücknehmen" }),
).toBeVisible();
// Die Hilfsfrist steht danach da — auch wenn sie null Minuten beträgt.
await expect(page.getByText("Hilfsfrist")).toBeVisible();
});
test("erfasst Mannschaft über den Vorschlag und zeigt die Stärke", async ({ page }) => {
test("erfasst Mannschaft über den Vorschlag und zeigt die Stärke", async ({
page,
}) => {
await page.goto(einsatzAdresse);
await page.getByRole("button", { name: "Mannschaft erfassen" }).click();
await expect(page.getByText(/sortiert nach Einsätzen im letzten Jahr/)).toBeVisible();
await expect(
page.getByText(/sortiert nach Einsätzen im letzten Jahr/),
).toBeVisible();
// Die ersten drei Vorschläge übernehmen.
const vorschlaege = page.locator("button", { hasText: /\d+ Einsätze/ });
@@ -113,19 +120,26 @@ test.describe("Einsatz erfassen und bearbeiten", () => {
test("nimmt eine Lagemeldung auf", async ({ page }) => {
await page.goto(einsatzAdresse);
await page.getByLabel("Neue Lagemeldung").fill("Lage erkundet, keine Gefahr für Personen.");
await page
.getByLabel("Neue Lagemeldung")
.fill("Lage erkundet, keine Gefahr für Personen.");
await page.getByRole("button", { name: "Melden" }).click();
await expect(page.getByText("Lage erkundet, keine Gefahr für Personen.")).toBeVisible();
await expect(
page.getByText("Lage erkundet, keine Gefahr für Personen."),
).toBeVisible();
});
test("gibt einen Portal-Link aus, der ohne Anmeldung funktioniert", async ({ page, browser }) => {
test("gibt einen Portal-Link aus, der ohne Anmeldung funktioniert", async ({
page,
browser,
}) => {
await page.goto(einsatzAdresse);
await page.getByLabel("Für wen ist der Link?").fill("FF Nachbarhausen");
await page.getByRole("button", { name: "Erzeugen" }).click();
const linkfeld = page.locator('input[readonly]');
const linkfeld = page.locator("input[readonly]");
await expect(linkfeld).toBeVisible();
const adresse = await linkfeld.inputValue();
expect(adresse).toContain("/portal/");
@@ -136,18 +150,28 @@ test.describe("Einsatz erfassen und bearbeiten", () => {
await fremdeSeite.goto(adresse);
await expect(fremdeSeite.getByRole("heading", { name: /Testeinsatz/ })).toBeVisible();
await expect(fremdeSeite.getByText("Zugang für FF Nachbarhausen")).toBeVisible();
await expect(
fremdeSeite.getByRole("heading", { name: /Testeinsatz/ }),
).toBeVisible();
await expect(
fremdeSeite.getByText("Zugang für FF Nachbarhausen"),
).toBeVisible();
// Und eine Meldung absetzen.
await fremdeSeite.getByLabel("Wer meldet?").fill("Gruppenführer Huber");
await fremdeSeite.getByLabel("Meldung").fill("Wir sind mit einer Staffel eingetroffen.");
await fremdeSeite
.getByLabel("Meldung")
.fill("Wir sind mit einer Staffel eingetroffen.");
await fremdeSeite.getByRole("button", { name: "Melden" }).click();
await expect(fremdeSeite.getByText("Wir sind mit einer Staffel eingetroffen.")).toBeVisible();
await expect(
fremdeSeite.getByText("Wir sind mit einer Staffel eingetroffen."),
).toBeVisible();
// Kein Weg in den Rest der Anwendung.
await expect(fremdeSeite.getByRole("link", { name: "Geräte" })).toHaveCount(0);
await expect(fremdeSeite.getByRole("link", { name: "Geräte" })).toHaveCount(
0,
);
await fremd.close();
});
@@ -156,7 +180,9 @@ test.describe("Einsatz erfassen und bearbeiten", () => {
const fremd = await browser.newContext();
const seite = await fremd.newPage();
await seite.goto("/portal/3f2504e0-4f89-11d3-9a0c-0305e82c3301.abcdefghijklmnopqrstuvwxyz01");
await seite.goto(
"/portal/3f2504e0-4f89-11d3-9a0c-0305e82c3301.abcdefghijklmnopqrstuvwxyz01",
);
await expect(seite.getByTestId("portalfehler")).toBeVisible();
await expect(seite.getByText(/nicht gültig/)).toBeVisible();
@@ -169,10 +195,16 @@ test.describe("Statistik", () => {
test("zeigt Hilfsfrist, Kategorien und Beteiligung", async ({ page }) => {
await page.goto("/einsaetze/statistik");
await expect(page.getByRole("heading", { name: "Einsatzstatistik" })).toBeVisible();
await expect(
page.getByRole("heading", { name: "Einsatzstatistik" }),
).toBeVisible();
await expect(page.getByText("Hilfsfrist (80 %)")).toBeVisible();
await expect(page.getByRole("heading", { name: "Nach Kategorie" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Beteiligung" })).toBeVisible();
await expect(
page.getByRole("heading", { name: "Nach Kategorie" }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Beteiligung" }),
).toBeVisible();
// Aus dem Seed: die Kategorien der acht Einsätze.
await expect(page.getByText("Brand groß").first()).toBeVisible();
@@ -187,11 +219,18 @@ test.describe("Übungen", () => {
await expect(page.getByText("Innenangriff mit Atemschutz")).toBeVisible();
});
test("öffnet eine Übung und zeigt die Anwesenheitsliste", async ({ page }) => {
test("öffnet eine Übung und zeigt die Anwesenheitsliste", async ({
page,
}) => {
await page.goto("/uebungen");
await page.getByRole("link", { name: /\d{4}-\d{4}/ }).first().click();
await page
.getByRole("link", { name: /\d{4}-\d{4}/ })
.first()
.click();
await expect(page.getByRole("heading", { name: "Anwesenheit" })).toBeVisible();
await expect(
page.getByRole("heading", { name: "Anwesenheit" }),
).toBeVisible();
await expect(page.getByText(/von \d+ aktiven Mitgliedern/)).toBeVisible();
});
});