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>
238 lines
9.5 KiB
TypeScript
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 },
|
|
);
|
|
});
|
|
});
|