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
+103
-55
@@ -1,4 +1,5 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { anmelden, eigenerName } from "./wehr";
|
||||
|
||||
/**
|
||||
* E2E-Tests für Phase 7 — Termine, Benachrichtigungen und Dienstausweis.
|
||||
@@ -18,37 +19,27 @@ 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";
|
||||
|
||||
/** Eindeutiger Titel je Lauf, damit sich parallele Läufe nicht ins Gehege kommen. */
|
||||
function eigenerTitel(zweck: string): string {
|
||||
return `E2E-${zweck}-${Date.now().toString(36)}`;
|
||||
}
|
||||
|
||||
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("Terminkalender", () => {
|
||||
test("zeigt die Termine aus dem Bestand", async ({ page }) => {
|
||||
await anmelden(page);
|
||||
await page.goto("/termine");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Termine", level: 1 })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Termine", level: 1 }),
|
||||
).toBeVisible();
|
||||
|
||||
// Aus dem Seed: der Übungsabend als Serie und der Lehrgang mit Plätzen.
|
||||
await expect(page.getByRole("link", { name: "Übungsabend", exact: true }).first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Übungsabend", exact: true }).first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Maschinistenlehrgang Teil 1" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("zeigt einen abgesagten Termin mit Grund statt ihn zu verstecken", async ({ page }) => {
|
||||
test("zeigt einen abgesagten Termin mit Grund statt ihn zu verstecken", async ({
|
||||
page,
|
||||
}) => {
|
||||
await anmelden(page);
|
||||
await page.goto("/termine");
|
||||
|
||||
@@ -67,7 +58,9 @@ test.describe("Terminkalender", () => {
|
||||
const jhv = page.getByRole("link", { name: "Jahreshauptversammlung" });
|
||||
await jhv.click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Jahreshauptversammlung" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Jahreshauptversammlung" }),
|
||||
).toBeVisible();
|
||||
|
||||
// "Absagen" exakt: sonst trifft es auch "Termin absagen" daneben.
|
||||
await page.getByRole("button", { name: "Zusagen", exact: true }).click();
|
||||
@@ -87,22 +80,30 @@ test.describe("Terminkalender", () => {
|
||||
* schon einen Platz hat, hängt an der Reihenfolge im Seed. Was hier zählt,
|
||||
* ist die Anzeige der Warteliste — die gilt für alle.
|
||||
*/
|
||||
test("zeigt Belegung und Warteliste eines begrenzten Lehrgangs", async ({ page }) => {
|
||||
test("zeigt Belegung und Warteliste eines begrenzten Lehrgangs", async ({
|
||||
page,
|
||||
}) => {
|
||||
await anmelden(page);
|
||||
await page.goto("/termine");
|
||||
|
||||
await page.getByRole("link", { name: "Maschinistenlehrgang Teil 1" }).click();
|
||||
await page
|
||||
.getByRole("link", { name: "Maschinistenlehrgang Teil 1" })
|
||||
.click();
|
||||
|
||||
await expect(page.getByText(/von 8 belegt/)).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: /^Zugesagt \(8\)$/ })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: /^Warteliste \(\d+\)$/ })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /^Zugesagt \(8\)$/ }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /^Warteliste \(\d+\)$/ }),
|
||||
).toBeVisible();
|
||||
|
||||
// Die Warteliste ist nummeriert — sonst weiß niemand, wer nachrückt.
|
||||
await expect(page.getByText("1.", { exact: false }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("legt einen Termin an", async ({ page }) => {
|
||||
const titel = eigenerTitel("Einzeltermin");
|
||||
const titel = eigenerName("Einzeltermin");
|
||||
|
||||
await anmelden(page);
|
||||
await page.goto("/termine");
|
||||
@@ -121,24 +122,28 @@ test.describe("Terminkalender", () => {
|
||||
* Jahr" einstellt, soll vorher lesen, dass daraus über fünfzig Einträge
|
||||
* werden — nicht danach.
|
||||
*/
|
||||
test("nennt vor dem Anlegen, wie viele Termine eine Serie ergibt", async ({ page }) => {
|
||||
test("nennt vor dem Anlegen, wie viele Termine eine Serie ergibt", async ({
|
||||
page,
|
||||
}) => {
|
||||
await anmelden(page);
|
||||
await page.goto("/termine");
|
||||
|
||||
await page.getByRole("button", { name: "Termin anlegen" }).click();
|
||||
await page.getByLabel("Titel").fill(eigenerTitel("Serienvorschau"));
|
||||
await page.getByLabel("Titel").fill(eigenerName("Serienvorschau"));
|
||||
|
||||
await page.getByLabel("Wiederholung").click();
|
||||
await page.getByRole("option", { name: "wöchentlich" }).click();
|
||||
|
||||
const inAchtWochen = new Date(Date.now() + 56 * 864e5);
|
||||
await page.getByLabel("Serie läuft bis").fill(inAchtWochen.toISOString().slice(0, 10));
|
||||
await page
|
||||
.getByLabel("Serie läuft bis")
|
||||
.fill(inAchtWochen.toISOString().slice(0, 10));
|
||||
|
||||
await expect(page.getByText(/Das legt \d+ Termine an/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("verlangt für eine Absage einen Grund", async ({ page }) => {
|
||||
const titel = eigenerTitel("Absage");
|
||||
const titel = eigenerName("Absage");
|
||||
|
||||
await anmelden(page);
|
||||
await page.goto("/termine");
|
||||
@@ -155,16 +160,22 @@ test.describe("Terminkalender", () => {
|
||||
const dialog = page.getByRole("dialog");
|
||||
|
||||
// Ohne Grund geht es nicht weiter.
|
||||
await expect(dialog.getByRole("button", { name: "Absagen", exact: true })).toBeDisabled();
|
||||
await expect(
|
||||
dialog.getByRole("button", { name: "Absagen", exact: true }),
|
||||
).toBeDisabled();
|
||||
|
||||
await dialog.getByLabel("Grund").fill("Sturmwarnung, Übungsgelände nicht befahrbar.");
|
||||
await dialog
|
||||
.getByLabel("Grund")
|
||||
.fill("Sturmwarnung, Übungsgelände nicht befahrbar.");
|
||||
await dialog.getByRole("button", { name: "Absagen", exact: true }).click();
|
||||
|
||||
await expect(page.getByText("Dieser Termin fällt aus.")).toBeVisible();
|
||||
await expect(page.getByText(/Sturmwarnung/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("führt eine Anwesenheitsliste über alle aktiven Mitglieder", async ({ page }) => {
|
||||
test("führt eine Anwesenheitsliste über alle aktiven Mitglieder", async ({
|
||||
page,
|
||||
}) => {
|
||||
await anmelden(page);
|
||||
await page.goto("/termine");
|
||||
|
||||
@@ -172,20 +183,30 @@ test.describe("Terminkalender", () => {
|
||||
|
||||
await page.getByRole("button", { name: "Anwesenheitsliste" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Anwesenheitsliste" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Anwesenheitsliste" }),
|
||||
).toBeVisible();
|
||||
// Auf einem Blatt zum Abhaken steht jeder — nicht nur die Angemeldeten.
|
||||
await expect(page.getByRole("columnheader", { name: "Anwesend" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("columnheader", { name: "Anwesend" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("row")).not.toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Benachrichtigungen", () => {
|
||||
test("sagt deutlich, dass ohne eigenes Zutun nichts kommt", async ({ page }) => {
|
||||
test("sagt deutlich, dass ohne eigenes Zutun nichts kommt", async ({
|
||||
page,
|
||||
}) => {
|
||||
await anmelden(page);
|
||||
await page.goto("/benachrichtigungen");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Benachrichtigungen", level: 1 })).toBeVisible();
|
||||
await expect(page.getByText(/Niemand wird ungefragt benachrichtigt/)).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Benachrichtigungen", level: 1 }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/Niemand wird ungefragt benachrichtigt/),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -198,19 +219,23 @@ test.describe("Benachrichtigungen", () => {
|
||||
|
||||
await page.getByLabel("Über").click();
|
||||
await expect(page.getByRole("option", { name: /Telegram/ })).toBeVisible();
|
||||
await expect(page.getByRole("option", { name: /Browser-Push/ })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("option", { name: /Browser-Push/ }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("erzeugt eine Kalenderadresse, die genau einmal sichtbar ist", async ({ page }) => {
|
||||
test("erzeugt eine Kalenderadresse, die genau einmal sichtbar ist", async ({
|
||||
page,
|
||||
}) => {
|
||||
await anmelden(page);
|
||||
await page.goto("/benachrichtigungen");
|
||||
|
||||
await page.getByLabel("Für welches Gerät?").fill(eigenerTitel("Kalender"));
|
||||
await page.getByLabel("Für welches Gerät?").fill(eigenerName("Kalender"));
|
||||
await page.getByRole("button", { name: "Adresse erzeugen" }).click();
|
||||
|
||||
await expect(page.getByText(/wird nur jetzt angezeigt/)).toBeVisible();
|
||||
|
||||
const feld = page.locator('input[readonly]');
|
||||
const feld = page.locator("input[readonly]");
|
||||
await expect(feld).toHaveValue(/\/api\/kalender\/.+\.ics$/);
|
||||
|
||||
// Nach dem Neuladen ist sie weg — gespeichert wird nur der Hash.
|
||||
@@ -218,14 +243,17 @@ test.describe("Benachrichtigungen", () => {
|
||||
await expect(page.getByText(/wird nur jetzt angezeigt/)).toBeHidden();
|
||||
});
|
||||
|
||||
test("liefert unter der Kalenderadresse eine gültige ICS-Datei", async ({ page, request }) => {
|
||||
test("liefert unter der Kalenderadresse eine gültige ICS-Datei", async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
await anmelden(page);
|
||||
await page.goto("/benachrichtigungen");
|
||||
|
||||
await page.getByLabel("Für welches Gerät?").fill(eigenerTitel("ICS"));
|
||||
await page.getByLabel("Für welches Gerät?").fill(eigenerName("ICS"));
|
||||
await page.getByRole("button", { name: "Adresse erzeugen" }).click();
|
||||
|
||||
const adresse = await page.locator('input[readonly]').inputValue();
|
||||
const adresse = await page.locator("input[readonly]").inputValue();
|
||||
const antwort = await request.get(adresse);
|
||||
|
||||
expect(antwort.status()).toBe(200);
|
||||
@@ -242,21 +270,29 @@ test.describe("Benachrichtigungen", () => {
|
||||
expect(antwort.status()).toBe(404);
|
||||
});
|
||||
|
||||
test("zeigt die Textvorlagen mit Platzhaltern und Vorschau", async ({ page }) => {
|
||||
test("zeigt die Textvorlagen mit Platzhaltern und Vorschau", async ({
|
||||
page,
|
||||
}) => {
|
||||
await anmelden(page);
|
||||
await page.goto("/benachrichtigungen/vorlagen");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Textvorlagen", level: 1 })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Textvorlagen", level: 1 }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Vorschau" })).toBeVisible();
|
||||
|
||||
// Die Platzhalter stehen zum Anklicken bereit — als Knopf, nicht nur im Text.
|
||||
await expect(page.getByRole("button", { name: "{titel}" })).toBeVisible();
|
||||
|
||||
// In der Vorschau stehen die Beispielwerte, nicht die Platzhalter.
|
||||
await expect(page.getByText("Übungsabend", { exact: false }).first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Übungsabend", { exact: false }).first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("warnt bei einem Platzhalter, den es für das Ereignis nicht gibt", async ({ page }) => {
|
||||
test("warnt bei einem Platzhalter, den es für das Ereignis nicht gibt", async ({
|
||||
page,
|
||||
}) => {
|
||||
await anmelden(page);
|
||||
await page.goto("/benachrichtigungen/vorlagen");
|
||||
|
||||
@@ -264,7 +300,9 @@ test.describe("Benachrichtigungen", () => {
|
||||
await feld.fill("Am {datum} um {uhrzeit}. Und ein {tippfehler}.");
|
||||
await page.getByRole("button", { name: "Speichern" }).click();
|
||||
|
||||
await expect(page.getByTestId("vorlagenfehler")).toContainText("tippfehler");
|
||||
await expect(page.getByTestId("vorlagenfehler")).toContainText(
|
||||
"tippfehler",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -273,10 +311,16 @@ test.describe("Dienstausweis", () => {
|
||||
await anmelden(page);
|
||||
await page.goto("/ausweis");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Dienstausweis", level: 1 })).toBeVisible();
|
||||
await expect(page.getByRole("img", { name: "Dienstausweis als QR-Code" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Dienstausweis", level: 1 }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("img", { name: "Dienstausweis als QR-Code" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText(/Gültig noch/)).toBeVisible();
|
||||
await expect(page.getByText(/Nach fünf Minuten ist er wertlos/)).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/Nach fünf Minuten ist er wertlos/),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -284,13 +328,17 @@ test.describe("Dienstausweis", () => {
|
||||
* scannt, hat keinen Zugang zu FLORIAN. Ein erfundener Code muss trotzdem
|
||||
* abgewiesen werden, und zwar mit einem Satz, den ein Mensch versteht.
|
||||
*/
|
||||
test("weist einen erfundenen Ausweis ab, ohne Anmeldung", async ({ browser }) => {
|
||||
test("weist einen erfundenen Ausweis ab, ohne Anmeldung", async ({
|
||||
browser,
|
||||
}) => {
|
||||
const kontext = await browser.newContext();
|
||||
const seite = await kontext.newPage();
|
||||
|
||||
await seite.goto("/ausweis/ausweis.a.b.c.d");
|
||||
|
||||
await expect(seite.getByRole("heading", { name: "Ausweis nicht gültig" })).toBeVisible();
|
||||
await expect(
|
||||
seite.getByRole("heading", { name: "Ausweis nicht gültig" }),
|
||||
).toBeVisible();
|
||||
await expect(seite.getByText(/Diese Seite zeigt an/)).toBeVisible();
|
||||
|
||||
await kontext.close();
|
||||
|
||||
Reference in New Issue
Block a user