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:
Florian Wilde
2026-09-04 13:24:48 +02:00
co-authored by Claude Opus 5
parent 8081131eb7
commit 7196c7110c
94 changed files with 1447 additions and 428 deletions
+45 -24
View File
@@ -1,4 +1,5 @@
import { expect, test, type Locator, type Page } from "@playwright/test";
import { anmelden } from "./wehr";
/**
* E2E-Tests für das Geräteformular.
@@ -18,16 +19,18 @@ import { expect, test, type Locator, type Page } from "@playwright/test";
* werden deshalb getippt, so wie es ein Mensch auch täte.
*/
const ADMIN_EMAIL = process.env.SEED_ADMIN_EMAIL ?? "admin@beispielwehr.de";
const ADMIN_PASSWORT = process.env.SEED_ADMIN_PASSWORT ?? "Feuerwehr2026";
/** Eindeutig je Testlauf, damit sich parallele Läufe nicht ins Gehege kommen. */
function testnummer(kennzeichen: string): string {
return `E2E-${kennzeichen}-${Date.now().toString().slice(-6)}`;
}
/** Tippt ein Datum in ein date-Feld. Reihenfolge nach deutscher Locale. */
async function datumTippen(feld: Locator, tag: string, monat: string, jahr: string) {
async function datumTippen(
feld: Locator,
tag: string,
monat: string,
jahr: string,
) {
await feld.click();
await feld.pressSequentially(`${tag}${monat}${jahr}`, { delay: 30 });
}
@@ -44,16 +47,12 @@ function auswahl(page: Page, name: string): Locator {
return page.getByRole("combobox", { name, exact: true });
}
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("/");
}
/** Füllt die beiden Pflichtfelder und wählt eine Lagerung. */
async function grunddatenAusfuellen(page: Page, nummer: string, bezeichnung: string) {
async function grunddatenAusfuellen(
page: Page,
nummer: string,
bezeichnung: string,
) {
await feld(page, "Inventarnummer").fill(nummer);
await feld(page, "Bezeichnung").fill(bezeichnung);
}
@@ -63,11 +62,15 @@ test.beforeEach(async ({ page }) => {
});
test.describe("Gerät anlegen", () => {
test("legt ein Gerät an einem Standort an und öffnet es danach", async ({ page }) => {
test("legt ein Gerät an einem Standort an und öffnet es danach", async ({
page,
}) => {
const nummer = testnummer("STANDORT");
await page.goto("/geraete/neu");
await expect(page.getByRole("heading", { name: "Neues Gerät" })).toBeVisible();
await expect(
page.getByRole("heading", { name: "Neues Gerät" }),
).toBeVisible();
await grunddatenAusfuellen(page, nummer, "Testgerät Standort");
await feld(page, "Hersteller").fill("Prüffirma");
@@ -78,11 +81,15 @@ test.describe("Gerät anlegen", () => {
await page.getByRole("button", { name: "Gerät anlegen" }).click();
await page.waitForURL(/\/geraete\/[0-9a-f-]{36}$/);
await expect(page.getByRole("heading", { name: "Testgerät Standort" })).toBeVisible();
await expect(
page.getByRole("heading", { name: "Testgerät Standort" }),
).toBeVisible();
await expect(page.getByText(nummer).first()).toBeVisible();
});
test("legt ein Gerät auf einem Fahrzeug samt Geräteraum an", async ({ page }) => {
test("legt ein Gerät auf einem Fahrzeug samt Geräteraum an", async ({
page,
}) => {
const nummer = testnummer("FAHRZEUG");
await page.goto("/geraete/neu");
@@ -91,7 +98,9 @@ test.describe("Gerät anlegen", () => {
await page.getByRole("radio", { name: "Auf einem Fahrzeug" }).click();
await auswahl(page, "Fahrzeug").click();
await page.getByRole("option", { name: /Florian Musterhausen 40\/1/ }).click();
await page
.getByRole("option", { name: /Florian Musterhausen 40\/1/ })
.click();
await auswahl(page, "Geräteraum").click();
await page.getByRole("option", { name: /^G1/ }).click();
@@ -113,12 +122,16 @@ test.describe("Gerät anlegen", () => {
await page.getByRole("radio", { name: "Auf einem Fahrzeug" }).click();
await page.getByRole("button", { name: "Gerät anlegen" }).click();
await expect(page.getByTestId("formularfehler")).toContainText("Fahrzeug auswählen");
await expect(page.getByTestId("formularfehler")).toContainText(
"Fahrzeug auswählen",
);
// Nichts gespeichert: die Adresse hat sich nicht geändert.
await expect(page).toHaveURL(/\/geraete\/neu$/);
});
test("rechnet die Fristen schon beim Ausfüllen vor", async ({ page }, angaben) => {
test("rechnet die Fristen schon beim Ausfüllen vor", async ({
page,
}, angaben) => {
// Unter Touch-Emulation nimmt Chromium keine zeichenweise Eingabe in
// Datumsfelder an — dort öffnet sich der Auswähler des Betriebssystems.
// Die Rechnung selbst prüfen die Unit-Tests in qr-und-fristen.test.ts;
@@ -142,7 +155,9 @@ test.describe("Gerät anlegen", () => {
await expect(page.getByText("01.03.2036")).toBeVisible();
});
test("übernimmt Prüfintervall und Lebensdauer aus dem Gerätetyp", async ({ page }, angaben) => {
test("übernimmt Prüfintervall und Lebensdauer aus dem Gerätetyp", async ({
page,
}, angaben) => {
test.skip(
angaben.project.name === "tablet",
"Datumsfelder lassen sich unter Touch-Emulation nicht tippen.",
@@ -166,7 +181,9 @@ test.describe("Gerät anlegen", () => {
});
test.describe("Gerät bearbeiten", () => {
test("ändert ein vorhandenes Gerät und protokolliert es", async ({ page }) => {
test("ändert ein vorhandenes Gerät und protokolliert es", async ({
page,
}) => {
await page.goto("/geraete");
await page.getByRole("link", { name: "INV-0002" }).click();
await page.getByRole("link", { name: "Bearbeiten" }).click();
@@ -182,10 +199,14 @@ test.describe("Gerät bearbeiten", () => {
// Die Änderung steht im Verlauf. Bei wiederholten Läufen sammeln sich dort
// mehrere Einträge — der Log ist append-only, das ist so gewollt.
await expect(page.getByText(/Gerät INV-0002 geändert/).first()).toBeVisible();
await expect(
page.getByText(/Gerät INV-0002 geändert/).first(),
).toBeVisible();
});
test("schaltet die Lagerung von Fahrzeug auf Standort um", async ({ page }) => {
test("schaltet die Lagerung von Fahrzeug auf Standort um", async ({
page,
}) => {
await page.goto("/geraete");
await page.getByRole("link", { name: "INV-0003" }).click();
await page.getByRole("link", { name: "Bearbeiten" }).click();