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