Files
FireAdministration/tests/e2e/karte.spec.ts
T
Florian WildeandClaude Opus 5 4cda79434f Bildschirmfotos fuer Phase 6 erneuern
Karte, Objekte und Wasserfoerderung dazu; die uebrigen Bilder auf den
aktuellen Stand gebracht (die Seitenleiste zeigt jetzt die Karte).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 03:11:07 +02:00

238 lines
9.5 KiB
TypeScript

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<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("Karte", () => {
test("zeigt Löschwasser und Objekte aus dem Bestand", async ({ page }) => {
await anmelden(page);
await page.goto("/karte");
// Erst die Oberfläche, dann die Zahlen: der Knopf trägt die Anzahl schon,
// bevor die Abfrage zurück ist, und steht dann auf null. Unter Last dauert
// das — deshalb wird auf den Wert gewartet, nicht auf ein Erscheinen.
await expect(page.getByTestId("karte")).toBeVisible();
await expect
.poll(
async () => {
const beschriftung = await page
.getByRole("button", { name: /Hydranten \(\d+\)/ })
.innerText();
return Number(beschriftung.replace(/\D/g, ""));
},
{ timeout: 30_000 },
)
// Aus dem Seed: 24 Löschwasserstellen, davon zwei defekt und zwei
// ungeprüft.
.toBeGreaterThanOrEqual(20);
await expect(page.getByRole("button", { name: /Objekte \(\d+\)/ })).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 },
);
});
});