import { expect, test, type Locator, type Page } from "@playwright/test"; /** * E2E-Tests für Phase 6 — die Kartenmodule. * * Zwei Dinge sind hier wichtiger als die Karte selbst: * * 1. **Die Umkreissuche.** Sie beantwortet die Frage, wegen der es das Modul * gibt: was liegt in der Nähe der Einsatzstelle? Geprüft wird gegen die * Beispieldaten mit bekannten Entfernungen. * 2. **Die Karte bleibt brauchbar, wenn der Kartenserver fehlt.** Im * Testlauf läuft keiner — also ist genau das der Normalfall hier, und die * Anwendung muss ihn ehrlich anzeigen statt kaputtzugehen. * * Voraussetzung: Stack läuft, Datenbank ist migriert und geseedet. */ 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"; /** * Die Fläche der Karte, bereit zum Anklicken. * * Zwei Dinge müssen stimmen, bevor ein Klick etwas bewirkt: die Karte muss im * Fenster liegen (page.mouse klickt in Fensterkoordinaten) und MapLibre muss * seine Zeichenfläche aufgebaut haben. Ohne das Warten landet der erste Klick * gelegentlich im Nichts — und der Test prüft dann eine Strecke aus einem * einzigen Punkt. */ async function kartenflaeche(karte: Locator): Promise<{ x: number; y: number; width: number; height: number }> { await karte.scrollIntoViewIfNeeded(); await expect(karte.locator("canvas")).toBeVisible(); const flaeche = await karte.boundingBox(); if (!flaeche) throw new Error("Karte hat keine Fläche."); return flaeche; } 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.describe("Karte", () => { test("zeigt Löschwasser und Objekte aus dem Bestand", async ({ page }) => { await anmelden(page); await page.goto("/karte"); // Aus dem Seed: 24 Löschwasserstellen, davon zwei defekt und zwei // ungeprüft, dazu drei Objekte. await expect(page.getByRole("button", { name: /Hydranten \(2[0-9]\)/ })).toBeVisible(); await expect(page.getByRole("button", { name: /Objekte \(\d+\)/ })).toBeVisible(); await expect(page.getByTestId("karte")).toBeVisible(); }); test("sagt es, wenn kein Kartenhintergrund eingerichtet ist", async ({ page }) => { await anmelden(page); await page.goto("/karte"); // Im Testlauf gibt es keinen Kartenserver. Die Anwendung muss das sagen // und trotzdem arbeiten — nicht eine leere Fläche zeigen und schweigen. await expect(page.getByTestId("karte-ohne-kacheln")).toContainText("Punkte liegen richtig"); }); test("findet über die Suche einen bestimmten Hydranten", async ({ page }) => { await anmelden(page); await page.goto("/karte/hydranten"); // „exact": daneben steht die Zelle mit dem Entfernen-Knopf, die den Namen // in ihrer Beschriftung trägt. await expect(page.getByRole("cell", { name: "H-0004", exact: true })).toBeVisible(); await expect(page.getByText("Weiher Kiesgrube")).toBeVisible(); }); test("zeigt Zustände im Klartext, nicht nur als Farbe", async ({ page }) => { await anmelden(page); await page.goto("/karte/hydranten"); // „defekt" und „ungeprüft" müssen lesbar dastehen — eine rote Markierung // allein sagt am Fahrzeug niemandem, was los ist. await expect(page.getByRole("cell", { name: "defekt" }).first()).toBeVisible(); await expect(page.getByRole("cell", { name: "ungeprüft" }).first()).toBeVisible(); }); }); test.describe("Objekte und Feuerwehrpläne", () => { test("listet die Objekte und öffnet eines", async ({ page }) => { await anmelden(page); await page.goto("/karte/objekte"); await expect(page.getByText("Grundschule Musterhausen")).toBeVisible(); await expect(page.getByText("Seniorenheim St. Anna")).toBeVisible(); await page.getByText("Seniorenheim St. Anna").click(); await page.waitForURL(/\/karte\/objekte\/[0-9a-f-]{36}$/); // Was auf der Anfahrt zählt, steht oben. await expect(page.getByTestId("objekt-detail")).toContainText("Zufahrt"); await expect(page.getByTestId("objekt-detail")).toContainText("Sauerstoffflaschen"); await expect(page.getByTestId("objekt-detail")).toContainText("FSD"); }); test("findet ein Objekt über ein Gefahrenstichwort", async ({ page }) => { await anmelden(page); await page.goto("/karte/objekte"); // Wer im Einsatz sucht, weiß selten den Namen — aber oft, was dort ist. await page.getByLabel("Objekte durchsuchen").fill("Photovoltaik"); await expect(page.getByText("Grundschule Musterhausen")).toBeVisible(); await expect(page.getByText("Seniorenheim St. Anna")).toHaveCount(0); }); test("zeigt die Ansprechpartner mit Erreichbarkeit", async ({ page }) => { await anmelden(page); await page.goto("/karte/objekte"); await page.getByText("Aussiedlerhof Huber").click(); await page.waitForURL(/\/karte\/objekte\/[0-9a-f-]{36}$/); await expect(page.getByText("Josef Huber")).toBeVisible(); await expect(page.getByText("jederzeit").first()).toBeVisible(); }); }); test.describe("Wasserförderung", () => { test("rechnet eine Strecke durch, sobald zwei Punkte stehen", async ({ page }) => { test.setTimeout(120_000); await anmelden(page); await page.goto("/karte/wasserfoerderung"); await expect(page.getByTestId("foerderplan")).toContainText("Noch keine Strecke"); // Zwei Punkte in die Karte tippen — die Karte hat keine Kacheln, aber // Koordinaten hat sie trotzdem. const flaeche = await kartenflaeche(page.getByTestId("karte")); await page.mouse.click(flaeche.x + flaeche.width * 0.35, flaeche.y + flaeche.height * 0.5); await expect(page.getByRole("heading", { name: "Punkte" })).toBeVisible(); await page.mouse.click(flaeche.x + flaeche.width * 0.65, flaeche.y + flaeche.height * 0.5); // Ab jetzt steht eine Rechnung da: Pumpen, Längen, Höhenunterschied. await expect(page.getByTestId("pumpenzahl")).toBeVisible(); await expect(page.getByTestId("foerderplan")).toContainText("Schlauchlängen"); // Und der Satz, der immer dazugehört. await expect(page.getByText(/Planungshilfe/)).toBeVisible(); }); test("nennt die Pumpenzahl und die Abschnitte", async ({ page }) => { test.setTimeout(120_000); await anmelden(page); await page.goto("/karte/wasserfoerderung"); const flaeche = await kartenflaeche(page.getByTestId("karte")); // Eine lange Strecke quer über die Karte: die braucht mehr als eine Pumpe. // Zwischen den Klicks warten, bis der erste Punkt angekommen ist — sonst // prüft der Test bei einem verschluckten Klick eine Strecke aus einem // einzigen Punkt. await page.mouse.click(flaeche.x + flaeche.width * 0.1, flaeche.y + flaeche.height * 0.5); await expect(page.getByRole("heading", { name: "Punkte" })).toBeVisible(); await page.mouse.click(flaeche.x + flaeche.width * 0.9, flaeche.y + flaeche.height * 0.5); await expect(page.getByText("Abschnitte")).toBeVisible(); await expect(page.getByRole("columnheader", { name: "Längen" })).toBeVisible(); const pumpen = Number(await page.getByTestId("pumpenzahl").innerText()); expect(pumpen).toBeGreaterThanOrEqual(1); }); }); test.describe("Lagekarte", () => { test("trägt ein taktisches Zeichen ein und nimmt es zurück", async ({ page }) => { test.setTimeout(180_000); await anmelden(page); // Irgendein Einsatz aus dem Seed. await page.goto("/einsaetze"); await page.getByRole("link", { name: /\d{4}-\d{4}/ }).first().click(); await page.waitForURL(/\/einsaetze\/[0-9a-f-]{36}$/); const lagekarte = page.getByTestId("lagekarte"); await expect(lagekarte).toBeVisible(); // Werkzeug wählen, in die Karte tippen. await lagekarte.getByTestId("zeichen-loeschfahrzeug").click(); const flaeche = await kartenflaeche(lagekarte.getByTestId("karte")); await page.mouse.click(flaeche.x + flaeche.width / 2, flaeche.y + flaeche.height / 2); await expect(lagekarte.getByText("loeschfahrzeug").first()).toBeVisible({ timeout: 30_000 }); // Zurücknehmen: es verschwindet von der Karte. await lagekarte.getByRole("button", { name: "zurücknehmen" }).first().click(); await expect(lagekarte.getByText("loeschfahrzeug")).toHaveCount(0, { timeout: 30_000 }); }); test("zeigt den Zustand des Abgleichs", async ({ page }) => { test.setTimeout(120_000); await anmelden(page); await page.goto("/einsaetze"); await page.getByRole("link", { name: /\d{4}-\d{4}/ }).first().click(); await page.waitForURL(/\/einsaetze\/[0-9a-f-]{36}$/); // Wer nicht sieht, dass der Draht weg ist, hält eine alte Lage für die // aktuelle — deshalb steht der Zustand sichtbar da. await expect(page.getByTestId("lagekarte-verbindung")).toHaveAttribute( "data-verbunden", "true", { timeout: 30_000 }, ); }); });