import { expect, test, type Page } from "@playwright/test"; /** * 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. */ 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 { 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); }); 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(); // "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(); }); 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")).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(); }); });