Files
FireAdministration/tests/e2e/einsaetze.spec.ts
T
Florian WildeandClaude Opus 5 7196c7110c 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>
2026-09-04 13:24:48 +02:00

237 lines
7.8 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { anmelden } from "./wehr";
/**
* E2E-Tests für Phase 3.
*
* Geprüft wird der Weg vom Alarm bis zur Auswertung: schnell erfassen,
* Zeitstempel setzen, Mannschaft eintragen, melden — und am Ende die Zahlen,
* die die Gemeinde sehen will.
*
* Dazu der Portal-Link: er ist der einzige Weg in FLORIAN hinein, der ohne
* Anmeldung funktioniert, und wird deshalb ausdrücklich in einem Browser
* **ohne** Sitzung geprüft.
*
* Voraussetzung: Stack läuft, Datenbank ist migriert und geseedet.
*/
test.beforeEach(async ({ page }) => {
await anmelden(page);
});
test.describe("Einsatzliste", () => {
test("zeigt die Einsätze der Wehr", async ({ page }) => {
await page.goto("/einsaetze");
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();
// 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 }) => {
await page.goto("/einsaetze");
await page.getByLabel("Einsätze durchsuchen").fill("Ölspur");
await expect(page.getByText("THL 1 Ölspur")).toBeVisible();
await expect(page.getByText("B 3 Wohnhausbrand")).toHaveCount(0);
});
});
/**
* Nacheinander: die Tests bauen aufeinander auf — erst wird ein Einsatz
* erfasst, dann daran weitergearbeitet.
*/
test.describe.configure({ mode: "serial" });
test.describe("Einsatz erfassen und bearbeiten", () => {
/** Merkt sich die Adresse des in diesem Durchlauf angelegten Einsatzes. */
let einsatzAdresse = "";
test("legt einen Einsatz über die Schnellerfassung an", async ({ page }) => {
await page.goto("/einsaetze");
await page.getByRole("button", { name: "Einsatz erfassen" }).click();
await page
.getByLabel("Einsatzstichwort")
.fill(`THL 1 Testeinsatz ${test.info().project.name}`);
await page.getByLabel("Straße").fill("Teststraße");
await page.getByLabel("Ort", { exact: true }).fill("Musterhausen");
await page.getByRole("button", { name: "Einsatz anlegen" }).click();
// Danach steht die Detailseite offen.
await page.waitForURL(/\/einsaetze\/[0-9a-f-]{36}$/);
einsatzAdresse = page.url();
await expect(
page.getByRole("heading", { name: /THL 1 Testeinsatz/ }),
).toBeVisible();
await expect(page.getByText("läuft", { exact: true })).toBeVisible();
});
test("setzt Zeitstempel und rechnet die Kennzahlen", async ({ page }) => {
await page.goto(einsatzAdresse);
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 page.getByRole("button", { name: "eingetroffen jetzt" }).click();
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,
}) => {
await page.goto(einsatzAdresse);
await page.getByRole("button", { name: "Mannschaft erfassen" }).click();
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/ });
await vorschlaege.nth(0).click();
await expect(page.getByText(/Kräfte erfasst/)).toBeVisible();
// Die Stärke wird sofort angezeigt.
await expect(page.getByText(/^\d+\/\d+\/\d+\/\d+$/)).toBeVisible();
});
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.getByRole("button", { name: "Melden" }).click();
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,
}) => {
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]");
await expect(linkfeld).toBeVisible();
const adresse = await linkfeld.inputValue();
expect(adresse).toContain("/portal/");
// Jetzt ein Browser ohne Sitzung — genau so, wie ihn eine Fremdwehr hätte.
const fremd = await browser.newContext();
const fremdeSeite = await fremd.newPage();
await fremdeSeite.goto(adresse);
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.getByRole("button", { name: "Melden" }).click();
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 fremd.close();
});
test("weist einen erfundenen Portal-Link ab", async ({ browser }) => {
const fremd = await browser.newContext();
const seite = await fremd.newPage();
await seite.goto(
"/portal/3f2504e0-4f89-11d3-9a0c-0305e82c3301.abcdefghijklmnopqrstuvwxyz01",
);
await expect(seite.getByTestId("portalfehler")).toBeVisible();
await expect(seite.getByText(/nicht gültig/)).toBeVisible();
await fremd.close();
});
});
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.getByText("Hilfsfrist (80 %)")).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();
});
});
test.describe("Übungen", () => {
test("zeigt die Übungen und ihre Teilnehmerzahl", async ({ page }) => {
await page.goto("/uebungen");
await expect(page.getByRole("heading", { name: "Übungen" })).toBeVisible();
await expect(page.getByText("Innenangriff mit Atemschutz")).toBeVisible();
});
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 expect(
page.getByRole("heading", { name: "Anwesenheit" }),
).toBeVisible();
await expect(page.getByText(/von \d+ aktiven Mitgliedern/)).toBeVisible();
});
});