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
+112
-46
@@ -1,4 +1,5 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { anmelden } from "./wehr";
|
||||
|
||||
/**
|
||||
* E2E-Tests für Phase 2.
|
||||
@@ -15,50 +16,65 @@ import { expect, test, type Page } from "@playwright/test";
|
||||
* Voraussetzung: Stack läuft, Datenbank ist migriert und geseedet.
|
||||
*/
|
||||
|
||||
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("/");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await anmelden(page);
|
||||
});
|
||||
|
||||
test.describe("Fristen-Dashboard", () => {
|
||||
test("zeigt die vier Zeitfenster und die offenen Termine", async ({ page }) => {
|
||||
test("zeigt die vier Zeitfenster und die offenen Termine", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/pruefungen");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Prüfungen und Fristen" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Prüfungen und Fristen" }),
|
||||
).toBeVisible();
|
||||
|
||||
// Die vier Töpfe sind gleichzeitig Filter.
|
||||
for (const bezeichnung of ["überfällig", "diese Woche", "diesen Monat", "nächste 90 Tage"]) {
|
||||
await expect(page.getByRole("button", { name: new RegExp(bezeichnung) })).toBeVisible();
|
||||
for (const bezeichnung of [
|
||||
"überfällig",
|
||||
"diese Woche",
|
||||
"diesen Monat",
|
||||
"nächste 90 Tage",
|
||||
]) {
|
||||
await expect(
|
||||
page.getByRole("button", { name: new RegExp(bezeichnung) }),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Offene Termine" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Offene Termine" }),
|
||||
).toBeVisible();
|
||||
// Aus dem Seed entstehen Termine für die Geräte mit Prüfvorgabe.
|
||||
await expect(page.getByRole("cell", { name: /INV-\d{4}/ }).first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("cell", { name: /INV-\d{4}/ }).first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("filtert die Terminliste über ein Zeitfenster", async ({ page }) => {
|
||||
await page.goto("/pruefungen");
|
||||
await expect(page.getByRole("heading", { name: "Offene Termine" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Offene Termine" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: /überfällig/ }).first().click();
|
||||
await page
|
||||
.getByRole("button", { name: /überfällig/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Filter aufheben" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Filter aufheben" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("bündelt die Termine nach Fahrzeug und lässt umschalten", async ({ page }) => {
|
||||
test("bündelt die Termine nach Fahrzeug und lässt umschalten", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/pruefungen");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Verteilung" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Verteilung" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel("Bündelung wählen").click();
|
||||
await page.getByRole("option", { name: "nach Gerätetyp" }).click();
|
||||
@@ -76,19 +92,29 @@ test.describe("Wartungsvorlagen", () => {
|
||||
test("zeigt die Vorlagen mit Intervall und Prüfpunkten", async ({ page }) => {
|
||||
await page.goto("/pruefungen/vorlagen");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Wartungsvorlagen" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Wartungsvorlagen" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Pressluftatmer — Jahresprüfung" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: "DGUV Vorschrift 3, DIN VDE 0701-0702" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("cell", { name: "DGUV Vorschrift 3, DIN VDE 0701-0702" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("öffnet die Checkliste einer Vorlage", async ({ page }) => {
|
||||
await page.goto("/pruefungen/vorlagen");
|
||||
await page.getByRole("link", { name: "Pressluftatmer — Jahresprüfung" }).click();
|
||||
await page
|
||||
.getByRole("link", { name: "Pressluftatmer — Jahresprüfung" })
|
||||
.click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Checkliste" })).toBeVisible();
|
||||
await expect(page.getByText("Ansprechdruck der Warneinrichtung")).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Checkliste" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Ansprechdruck der Warneinrichtung"),
|
||||
).toBeVisible();
|
||||
// Der zulässige Bereich steht am Punkt.
|
||||
await expect(page.getByText(/zulässig 50 bis 60 bar/)).toBeVisible();
|
||||
});
|
||||
@@ -114,12 +140,17 @@ function geraetIndex(): number {
|
||||
async function pruefungStarten(page: Page): Promise<void> {
|
||||
await page.goto("/geraete");
|
||||
await page.getByLabel("Geräte durchsuchen").fill("Feuerwehrleine");
|
||||
await page.getByRole("link", { name: /INV-\d{4}/ }).nth(geraetIndex()).click();
|
||||
await page
|
||||
.getByRole("link", { name: /INV-\d{4}/ })
|
||||
.nth(geraetIndex())
|
||||
.click();
|
||||
await page.getByRole("link", { name: "Prüfung durchführen" }).click();
|
||||
}
|
||||
|
||||
test.describe("Prüfwizard", () => {
|
||||
test("führt von der Checkliste bis zum unterschriebenen Protokoll", async ({ page }) => {
|
||||
test("führt von der Checkliste bis zum unterschriebenen Protokoll", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Der laengste Weg der Anwendung: Wizard, Unterschrift, Nummernkreis und
|
||||
// Protokoll. Laeuft die ganze Suite parallel, reicht das Standardlimit
|
||||
// dafuer nicht — das ist Uebersetzungszeit des Entwicklungsservers, kein
|
||||
@@ -133,15 +164,22 @@ test.describe("Prüfwizard", () => {
|
||||
// Freitext. Vor jedem Schritt wird auf die Schrittanzeige gewartet — sonst
|
||||
// klickt der Test schneller, als die Oberfläche umschaltet.
|
||||
for (const nummer of [1, 2, 3, 4]) {
|
||||
await expect(page.getByText(new RegExp(`Schritt ${nummer} von 4`))).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(new RegExp(`Schritt ${nummer} von 4`)),
|
||||
).toBeVisible();
|
||||
|
||||
// "in Ordnung" exakt: sonst trifft der Name auch "nicht in Ordnung".
|
||||
const inOrdnung = page.getByRole("button", { name: "in Ordnung", exact: true });
|
||||
const inOrdnung = page.getByRole("button", {
|
||||
name: "in Ordnung",
|
||||
exact: true,
|
||||
});
|
||||
if ((await inOrdnung.count()) > 0) await inOrdnung.click();
|
||||
|
||||
const befund = page.getByLabel("Befund");
|
||||
if ((await befund.count()) > 0) {
|
||||
await befund.fill("Leine vollständig durch die Hand gezogen, ohne Befund.");
|
||||
await befund.fill(
|
||||
"Leine vollständig durch die Hand gezogen, ohne Befund.",
|
||||
);
|
||||
}
|
||||
|
||||
await page.getByRole("button", { name: "Weiter" }).click();
|
||||
@@ -149,7 +187,9 @@ test.describe("Prüfwizard", () => {
|
||||
|
||||
// Abschluss: Ergebnis ist vorbelegt, Unterschrift fehlt noch.
|
||||
await expect(page.getByRole("heading", { name: "Ergebnis" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Prüfung abschließen" })).toBeDisabled();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Prüfung abschließen" }),
|
||||
).toBeDisabled();
|
||||
|
||||
// Unterschrift zeichnen — mit der Maus, wie mit dem Finger auf dem Tablet.
|
||||
// Erst ins Bild holen: die Mauskoordinaten beziehen sich auf das sichtbare
|
||||
@@ -163,7 +203,9 @@ test.describe("Prüfwizard", () => {
|
||||
await page.mouse.move(rahmen!.x + 30, rahmen!.y + rahmen!.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(rahmen!.x + 90, rahmen!.y + 20, { steps: 8 });
|
||||
await page.mouse.move(rahmen!.x + 150, rahmen!.y + rahmen!.height - 20, { steps: 8 });
|
||||
await page.mouse.move(rahmen!.x + 150, rahmen!.y + rahmen!.height - 20, {
|
||||
steps: 8,
|
||||
});
|
||||
await page.mouse.move(rahmen!.x + 210, rahmen!.y + 30, { steps: 8 });
|
||||
await page.mouse.up();
|
||||
|
||||
@@ -171,20 +213,30 @@ test.describe("Prüfwizard", () => {
|
||||
await expect(page.getByRole("button", { name: "Löschen" })).toBeEnabled();
|
||||
|
||||
await page.getByRole("button", { name: "Übernehmen" }).click();
|
||||
await expect(page.getByRole("button", { name: "übernommen" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "übernommen" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Prüfung abschließen" }).click();
|
||||
|
||||
// Danach steht das Protokoll da — mit Nummer und Ergebnis.
|
||||
await page.waitForURL(/\/pruefungen\/[0-9a-f-]{36}$/);
|
||||
await expect(page.getByRole("heading", { name: /Prüfprotokoll \d{4}-\d{4}/ })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /Prüfprotokoll \d{4}-\d{4}/ }),
|
||||
).toBeVisible();
|
||||
// Genau der Kleinschreibung wegen "exact": darüber steht auch das Feld
|
||||
// "Abgeschlossen" mit dem Zeitpunkt.
|
||||
await expect(page.getByText("abgeschlossen", { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Checkliste" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("abgeschlossen", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Checkliste" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("verlangt eine Unterschrift, bevor abgeschlossen werden kann", async ({ page }) => {
|
||||
test("verlangt eine Unterschrift, bevor abgeschlossen werden kann", async ({
|
||||
page,
|
||||
}) => {
|
||||
await pruefungStarten(page);
|
||||
|
||||
// Erst warten, bis der Wizard steht — er legt die Prüfung beim Öffnen an.
|
||||
@@ -198,7 +250,9 @@ test.describe("Prüfwizard", () => {
|
||||
}
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Ergebnis" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Prüfung abschließen" })).toBeDisabled();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Prüfung abschließen" }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -207,17 +261,25 @@ test.describe("Mängel und Rückrufe", () => {
|
||||
await page.goto("/maengel");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Mängel" })).toBeVisible();
|
||||
await expect(page.getByText(/Leinenbeutel an einer Naht aufgescheuert/)).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/Leinenbeutel an einer Naht aufgescheuert/),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("zeigt den Rückruf und seine betroffenen Geräte", async ({ page }) => {
|
||||
await page.goto("/rueckrufe");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Rückrufe" })).toBeVisible();
|
||||
await page.getByRole("link", { name: /Sicherheitshinweis Ausatemventil/ }).click();
|
||||
await page
|
||||
.getByRole("link", { name: /Sicherheitshinweis Ausatemventil/ })
|
||||
.click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Betroffene Geräte" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Mögliche Treffer im Bestand" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Betroffene Geräte" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Mögliche Treffer im Bestand" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -225,7 +287,11 @@ test.describe("Protokolle", () => {
|
||||
test("listet abgeschlossene Prüfungen mit Ergebnis", async ({ page }) => {
|
||||
await page.goto("/pruefungen/verlauf");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Prüfprotokolle" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: /\d{4}-\d{4}/ }).first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Prüfprotokolle" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("link", { name: /\d{4}-\d{4}/ }).first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user