Aufraeumarbeiten: ESLint, DOM-Verschachtelung, eigene Testwehr
ESLint hatte keine Konfiguration — das Skript "npm run lint" pruefte nichts. Jetzt Flat Config ohne Formatierungsregeln, dafuer mit den Regeln, die die Fehler dieses Projekts gemeldet haetten. Er hat gleich zwei gefunden, die niemand gesucht hatte. Das Badge rendert ein span statt eines div. Damit ist die Meldung "div cannot be a descendant of p" weg, die seit Phase 3 auf der Einsatzseite stand — sie ist der klassische Ausloeser fuer einen Hydration-Fehler. Die E2E-Tests laufen gegen eine eigene Wehr. Nach fuenf Phasen standen in der Beispielwehr 120 Atemschutz-Ueberwachungen statt einer und 115 Geraete statt 45; bei jedem Lauf aenderten sich alle Bildschirmfotos. Der Bestand ist einmal zurueckgesetzt, npm run db:test-reset baut die Testwehr neu auf. Behoben nebenbei: - Die Fehlerflaeche der Karte konnte nie etwas anzeigen. - Zwei useMemo der Lagekarte rechneten bei jedem Rendern neu. - tests/e2e war vom Typecheck ausgenommen und liess einen doppelt deklarierten Bezeichner durch. Closes #10 Closes #11 Closes #12 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8081131eb7
commit
7196c7110c
+129
-56
@@ -1,4 +1,5 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { anmelden } from "./wehr";
|
||||
|
||||
/**
|
||||
* E2E-Tests für Phase 5 — die Atemschutzüberwachung.
|
||||
@@ -17,25 +18,19 @@ import { expect, test, type Page } from "@playwright/test";
|
||||
*/
|
||||
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";
|
||||
|
||||
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("/");
|
||||
}
|
||||
|
||||
/** Eröffnet eine Überwachung und gibt deren Adresse zurück. */
|
||||
async function ueberwachungEroeffnen(page: Page, bezeichnung: string): Promise<string> {
|
||||
async function ueberwachungEroeffnen(
|
||||
page: Page,
|
||||
bezeichnung: string,
|
||||
): Promise<string> {
|
||||
await page.goto("/atemschutz");
|
||||
await page.getByLabel("Bezeichnung").fill(bezeichnung);
|
||||
await page.getByRole("button", { name: "Eröffnen" }).click();
|
||||
|
||||
// Sie erscheint unter „Läuft gerade" — von dort geht es hinein.
|
||||
const eintrag = page.getByRole("link", { name: new RegExp(bezeichnung) }).first();
|
||||
const eintrag = page
|
||||
.getByRole("link", { name: new RegExp(bezeichnung) })
|
||||
.first();
|
||||
await expect(eintrag).toBeVisible();
|
||||
await eintrag.click();
|
||||
|
||||
@@ -79,17 +74,23 @@ test.describe("Atemschutz", () => {
|
||||
await expect(page.getByTestId("atemschutz-hinweis")).toBeVisible();
|
||||
});
|
||||
|
||||
test("zeigt die Tauglichkeit mit Klartext, nicht nur mit Farbe", async ({ page }) => {
|
||||
test("zeigt die Tauglichkeit mit Klartext, nicht nur mit Farbe", async ({
|
||||
page,
|
||||
}) => {
|
||||
await anmelden(page);
|
||||
await page.goto("/atemschutz");
|
||||
|
||||
// Aus dem Seed: einer hat eine abgelaufene G26, einem fehlt die
|
||||
// Belastungsübung. Beides muss lesbar dastehen.
|
||||
await expect(page.getByText("G26.3 ist abgelaufen").first()).toBeVisible();
|
||||
await expect(page.getByText(/Belastungsübung in diesem Jahr fehlt/).first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/Belastungsübung in diesem Jahr fehlt/).first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("führt eine Überwachung von der Eröffnung bis zum Protokoll", async ({ page }) => {
|
||||
test("führt eine Überwachung von der Eröffnung bis zum Protokoll", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(180_000);
|
||||
await anmelden(page);
|
||||
|
||||
@@ -151,13 +152,17 @@ test.describe("Atemschutz", () => {
|
||||
|
||||
// Großzügig: das Beenden stellt zusätzlich den Auftrag für das PDF in die
|
||||
// Warteschlange, und unter Last dauert die Runde über Redis.
|
||||
await expect(page.getByRole("button", { name: "Überwachung beenden" })).toBeHidden({
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Überwachung beenden" }),
|
||||
).toBeHidden({
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
// Und das Protokoll gibt es als PDF. Innerhalb der Ansicht gesucht: in der
|
||||
// Seitenleiste steht ebenfalls ein „Protokoll".
|
||||
const protokoll = page.getByTestId("ueberwachung").getByRole("link", { name: "Protokoll" });
|
||||
const protokoll = page
|
||||
.getByTestId("ueberwachung")
|
||||
.getByRole("link", { name: "Protokoll" });
|
||||
await expect(protokoll).toBeVisible();
|
||||
|
||||
const antwort = await page.request.get(
|
||||
@@ -167,11 +172,16 @@ test.describe("Atemschutz", () => {
|
||||
expect(antwort.headers()["content-type"]).toContain("application/pdf");
|
||||
});
|
||||
|
||||
test("lässt eine Überwachung mit einem Trupp im Einsatz nicht beenden", async ({ page }) => {
|
||||
test("lässt eine Überwachung mit einem Trupp im Einsatz nicht beenden", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(180_000);
|
||||
await anmelden(page);
|
||||
|
||||
await ueberwachungEroeffnen(page, `E2E Offen ${test.info().project.name} ${Date.now()}`);
|
||||
await ueberwachungEroeffnen(
|
||||
page,
|
||||
`E2E Offen ${test.info().project.name} ${Date.now()}`,
|
||||
);
|
||||
await truppAnlegen(page, "Sicherheitstrupp");
|
||||
|
||||
const trupp = page.locator("[data-testid^='trupp-']").first();
|
||||
@@ -183,15 +193,22 @@ test.describe("Atemschutz", () => {
|
||||
|
||||
// Eine beendete Überwachung mit einem Trupp, der noch drin steht, wäre die
|
||||
// gefährlichste Zeile im ganzen Protokoll.
|
||||
await expect(page.getByTestId("ueberwachungsfehler")).toContainText("nicht als draußen");
|
||||
await expect(page.getByRole("button", { name: "Überwachung beenden" })).toBeVisible();
|
||||
await expect(page.getByTestId("ueberwachungsfehler")).toContainText(
|
||||
"nicht als draußen",
|
||||
);
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Überwachung beenden" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("meldet und hebt einen Notfall auf", async ({ page }) => {
|
||||
test.setTimeout(180_000);
|
||||
await anmelden(page);
|
||||
|
||||
await ueberwachungEroeffnen(page, `E2E Mayday ${test.info().project.name} ${Date.now()}`);
|
||||
await ueberwachungEroeffnen(
|
||||
page,
|
||||
`E2E Mayday ${test.info().project.name} ${Date.now()}`,
|
||||
);
|
||||
await truppAnlegen(page, "Notfalltrupp");
|
||||
|
||||
const trupp = page.locator("[data-testid^='trupp-']").first();
|
||||
@@ -218,7 +235,10 @@ test.describe("Die Uhr übersteht alles", () => {
|
||||
test.setTimeout(180_000);
|
||||
await anmelden(page);
|
||||
|
||||
await ueberwachungEroeffnen(page, `E2E Neuladen ${test.info().project.name} ${Date.now()}`);
|
||||
await ueberwachungEroeffnen(
|
||||
page,
|
||||
`E2E Neuladen ${test.info().project.name} ${Date.now()}`,
|
||||
);
|
||||
await truppAnlegen(page, "Uhrtrupp");
|
||||
|
||||
const trupp = page.locator("[data-testid^='trupp-']").first();
|
||||
@@ -227,18 +247,26 @@ test.describe("Die Uhr übersteht alles", () => {
|
||||
|
||||
// Ein paar Sekunden laufen lassen, damit es etwas zu verlieren gibt.
|
||||
await expect
|
||||
.poll(async () => alsSekunden(await page.getByTestId("trupp-dauer").innerText()), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.poll(
|
||||
async () =>
|
||||
alsSekunden(await page.getByTestId("trupp-dauer").innerText()),
|
||||
{
|
||||
timeout: 30_000,
|
||||
},
|
||||
)
|
||||
.toBeGreaterThanOrEqual(5);
|
||||
|
||||
const vorher = alsSekunden(await page.getByTestId("trupp-dauer").innerText());
|
||||
const vorher = alsSekunden(
|
||||
await page.getByTestId("trupp-dauer").innerText(),
|
||||
);
|
||||
|
||||
// --- Neuladen ------------------------------------------------------------
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("ueberwachung")).toBeVisible();
|
||||
|
||||
const nachher = alsSekunden(await page.getByTestId("trupp-dauer").innerText());
|
||||
const nachher = alsSekunden(
|
||||
await page.getByTestId("trupp-dauer").innerText(),
|
||||
);
|
||||
|
||||
// Die Uhr muss weitergelaufen sein — nicht zurückgesprungen, nicht bei
|
||||
// null angefangen. Ein Zähler im Browser hätte hier neu begonnen.
|
||||
@@ -246,18 +274,26 @@ test.describe("Die Uhr übersteht alles", () => {
|
||||
expect(nachher).toBeLessThan(vorher + 60);
|
||||
});
|
||||
|
||||
test("läuft weiter, wenn der Server nicht mehr antwortet", async ({ page, context }) => {
|
||||
test("läuft weiter, wenn der Server nicht mehr antwortet", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
test.setTimeout(180_000);
|
||||
await anmelden(page);
|
||||
|
||||
await ueberwachungEroeffnen(page, `E2E Netzverlust ${test.info().project.name} ${Date.now()}`);
|
||||
await ueberwachungEroeffnen(
|
||||
page,
|
||||
`E2E Netzverlust ${test.info().project.name} ${Date.now()}`,
|
||||
);
|
||||
await truppAnlegen(page, "Netzverlusttrupp");
|
||||
|
||||
const trupp = page.locator("[data-testid^='trupp-']").first();
|
||||
await trupp.getByRole("button", { name: "Einstieg" }).click();
|
||||
await expect(trupp).toHaveAttribute("data-status", "IM_EINSATZ");
|
||||
|
||||
const vorher = alsSekunden(await page.getByTestId("trupp-dauer").innerText());
|
||||
const vorher = alsSekunden(
|
||||
await page.getByTestId("trupp-dauer").innerText(),
|
||||
);
|
||||
|
||||
// --- Netz weg ------------------------------------------------------------
|
||||
await context.setOffline(true);
|
||||
@@ -265,16 +301,22 @@ test.describe("Die Uhr übersteht alles", () => {
|
||||
// Die Uhr braucht den Server nicht: sie rechnet aus dem Einstiegszeitpunkt,
|
||||
// den sie schon hat. Ein heruntergezählter Wert wäre hier stehen geblieben.
|
||||
await expect
|
||||
.poll(async () => alsSekunden(await page.getByTestId("trupp-dauer").innerText()), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.poll(
|
||||
async () =>
|
||||
alsSekunden(await page.getByTestId("trupp-dauer").innerText()),
|
||||
{
|
||||
timeout: 30_000,
|
||||
},
|
||||
)
|
||||
.toBeGreaterThan(vorher + 3);
|
||||
|
||||
// --- Netz zurück ---------------------------------------------------------
|
||||
await context.setOffline(false);
|
||||
|
||||
// Und der Server bestätigt, was der Browser gerechnet hat.
|
||||
const nachher = alsSekunden(await page.getByTestId("trupp-dauer").innerText());
|
||||
const nachher = alsSekunden(
|
||||
await page.getByTestId("trupp-dauer").innerText(),
|
||||
);
|
||||
expect(nachher).toBeGreaterThan(vorher);
|
||||
});
|
||||
|
||||
@@ -282,22 +324,35 @@ test.describe("Die Uhr übersteht alles", () => {
|
||||
test.setTimeout(180_000);
|
||||
await anmelden(page);
|
||||
|
||||
await ueberwachungEroeffnen(page, `E2E Stromabriss ${test.info().project.name} ${Date.now()}`);
|
||||
await ueberwachungEroeffnen(
|
||||
page,
|
||||
`E2E Stromabriss ${test.info().project.name} ${Date.now()}`,
|
||||
);
|
||||
await truppAnlegen(page, "Abrisstrupp");
|
||||
|
||||
const trupp = page.locator("[data-testid^='trupp-']").first();
|
||||
await trupp.getByRole("button", { name: "Einstieg" }).click();
|
||||
await expect(page.getByTestId("verbindung")).toHaveAttribute("data-verbunden", "true", {
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(page.getByTestId("verbindung")).toHaveAttribute(
|
||||
"data-verbunden",
|
||||
"true",
|
||||
{
|
||||
timeout: 30_000,
|
||||
},
|
||||
);
|
||||
|
||||
await expect
|
||||
.poll(async () => alsSekunden(await page.getByTestId("trupp-dauer").innerText()), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.poll(
|
||||
async () =>
|
||||
alsSekunden(await page.getByTestId("trupp-dauer").innerText()),
|
||||
{
|
||||
timeout: 30_000,
|
||||
},
|
||||
)
|
||||
.toBeGreaterThanOrEqual(4);
|
||||
|
||||
const vorher = alsSekunden(await page.getByTestId("trupp-dauer").innerText());
|
||||
const vorher = alsSekunden(
|
||||
await page.getByTestId("trupp-dauer").innerText(),
|
||||
);
|
||||
|
||||
/*
|
||||
Den Strom abschneiden.
|
||||
@@ -308,33 +363,51 @@ test.describe("Die Uhr übersteht alles", () => {
|
||||
geladen; der neue Ereignisstrom kommt dann gar nicht erst zustande. Damit
|
||||
prüft dieser Test beides auf einmal: Neuladen **und** toter Strom.
|
||||
*/
|
||||
await page.route("**/api/atemschutz/strom**", (route) => route.abort("connectionreset"));
|
||||
await page.route("**/api/atemschutz/strom**", (route) =>
|
||||
route.abort("connectionreset"),
|
||||
);
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByTestId("ueberwachung")).toBeVisible();
|
||||
await expect(page.getByTestId("verbindung")).toHaveAttribute("data-verbunden", "false", {
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(page.getByText("Ereignisstrom ist unterbrochen")).toBeVisible();
|
||||
await expect(page.getByTestId("verbindung")).toHaveAttribute(
|
||||
"data-verbunden",
|
||||
"false",
|
||||
{
|
||||
timeout: 30_000,
|
||||
},
|
||||
);
|
||||
await expect(
|
||||
page.getByText("Ereignisstrom ist unterbrochen"),
|
||||
).toBeVisible();
|
||||
|
||||
// Trotz totem Strom stimmt die Uhr: der Stand kommt weiter über die
|
||||
// normale Abfrage, und gerechnet wird aus Zeitstempeln.
|
||||
const nachAbriss = alsSekunden(await page.getByTestId("trupp-dauer").innerText());
|
||||
const nachAbriss = alsSekunden(
|
||||
await page.getByTestId("trupp-dauer").innerText(),
|
||||
);
|
||||
expect(nachAbriss).toBeGreaterThanOrEqual(vorher);
|
||||
|
||||
await expect
|
||||
.poll(async () => alsSekunden(await page.getByTestId("trupp-dauer").innerText()), {
|
||||
timeout: 30_000,
|
||||
})
|
||||
.poll(
|
||||
async () =>
|
||||
alsSekunden(await page.getByTestId("trupp-dauer").innerText()),
|
||||
{
|
||||
timeout: 30_000,
|
||||
},
|
||||
)
|
||||
.toBeGreaterThan(nachAbriss + 2);
|
||||
|
||||
// --- Strom zurück --------------------------------------------------------
|
||||
await page.unroute("**/api/atemschutz/strom**");
|
||||
|
||||
// EventSource verbindet sich von selbst neu — ohne eine Zeile dafür.
|
||||
await expect(page.getByTestId("verbindung")).toHaveAttribute("data-verbunden", "true", {
|
||||
timeout: 60_000,
|
||||
});
|
||||
await expect(page.getByTestId("verbindung")).toHaveAttribute(
|
||||
"data-verbunden",
|
||||
"true",
|
||||
{
|
||||
timeout: 60_000,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user