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
+85
-34
@@ -1,4 +1,5 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { ADMIN_EMAIL, ADMIN_PASSWORT, WEHR_SLUG } from "./wehr";
|
||||
|
||||
/**
|
||||
* E2E-Tests für Phase 4 — das Terminal im Gerätehaus.
|
||||
@@ -19,16 +20,14 @@ import { expect, test, type Page } from "@playwright/test";
|
||||
*/
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
const GERAETE_SCHLUESSEL = process.env.KIOSK_DEVICE_SECRET ?? "bitte_aendern_kiosk_geheimnis";
|
||||
const WEHR = "ff-musterhausen";
|
||||
const GERAETE_SCHLUESSEL =
|
||||
process.env.KIOSK_DEVICE_SECRET ?? "bitte_aendern_kiosk_geheimnis";
|
||||
const WEHR = WEHR_SLUG;
|
||||
|
||||
/** Aus dem Seed: Andreas Huber, Mitgliedsnummer 0001. */
|
||||
const MITGLIEDSNUMMER = "0001";
|
||||
const PIN = "471203";
|
||||
|
||||
const ADMIN_EMAIL = process.env.SEED_ADMIN_EMAIL ?? "admin@beispielwehr.de";
|
||||
const ADMIN_PASSWORT = process.env.SEED_ADMIN_PASSWORT ?? "Feuerwehr2026";
|
||||
|
||||
/**
|
||||
* Beide Playwright-Projekte (Desktop und Tablet) laufen gegen dieselbe
|
||||
* Datenbank. Damit sich die Testdaten nicht überlagern, trägt jedes seine
|
||||
@@ -43,12 +42,16 @@ async function terminalEinrichten(page: Page): Promise<void> {
|
||||
await page.goto(
|
||||
`/kiosk/einrichten?wehr=${WEHR}&schluessel=${encodeURIComponent(GERAETE_SCHLUESSEL)}`,
|
||||
);
|
||||
await expect(page.getByRole("heading", { name: "Terminal einrichten" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Terminal einrichten" }),
|
||||
).toBeVisible();
|
||||
|
||||
// Erst wenn die Felder aus der Adresse gefüllt sind, hat das Formular etwas
|
||||
// abzuschicken — sie kommen aus einem Effekt, also nach der Hydration.
|
||||
await expect(page.getByLabel("Kurzname der Wehr")).toHaveValue(WEHR);
|
||||
await expect(page.getByLabel("Gerätegeheimnis")).toHaveValue(GERAETE_SCHLUESSEL);
|
||||
await expect(page.getByLabel("Gerätegeheimnis")).toHaveValue(
|
||||
GERAETE_SCHLUESSEL,
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Terminal einrichten" }).click();
|
||||
await page.waitForURL("**/kiosk");
|
||||
@@ -81,17 +84,23 @@ async function verwaltungAnmelden(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
test.describe("Terminal", () => {
|
||||
test("verlangt die Einrichtung, bevor sich jemand anmelden kann", async ({ page }) => {
|
||||
test("verlangt die Einrichtung, bevor sich jemand anmelden kann", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Frischer Kontext, also leerer localStorage: das Tablet kennt keine Wehr.
|
||||
await page.goto("/kiosk");
|
||||
await expect(page.getByRole("heading", { name: "Terminal nicht eingerichtet" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Terminal nicht eingerichtet" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("meldet mit Mitgliedsnummer und PIN an", async ({ page }) => {
|
||||
await terminalAnmelden(page);
|
||||
|
||||
// Die Kacheln richten sich nach den Rechten — Kommen/Gehen hat jeder.
|
||||
await expect(page.getByText("Anwesenheit erfassen").or(page.getByText("da seit"))).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Anwesenheit erfassen").or(page.getByText("da seit")),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("Mangel melden")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -104,7 +113,9 @@ test.describe("Terminal", () => {
|
||||
await page.getByRole("button", { name: "Anmelden" }).click();
|
||||
|
||||
await expect(page.getByTestId("anmeldefehler")).toBeVisible();
|
||||
await expect(page.getByTestId("anmeldefehler")).toContainText("Mitgliedsnummer und PIN");
|
||||
await expect(page.getByTestId("anmeldefehler")).toContainText(
|
||||
"Mitgliedsnummer und PIN",
|
||||
);
|
||||
});
|
||||
|
||||
test("erfasst Kommen oder Gehen mit zwei Tipps", async ({ page }) => {
|
||||
@@ -134,20 +145,31 @@ test.describe("Offline-Betrieb", () => {
|
||||
let mangeltext = "";
|
||||
let waeschestueck = "";
|
||||
|
||||
test("erfasst ohne Netz und überträgt, sobald das Netz zurück ist", async ({ page, context }) => {
|
||||
test("erfasst ohne Netz und überträgt, sobald das Netz zurück ist", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await terminalAnmelden(page);
|
||||
|
||||
// Einmal mit Netz laden — so, wie das Tablet im Gerätehaus hängt, bevor
|
||||
// der Router streikt.
|
||||
await page.goto("/kiosk/mangel");
|
||||
await expect(page.getByRole("heading", { name: "Mangel melden" })).toBeVisible();
|
||||
await expect(page.getByTestId("kiosk-zustand")).toHaveAttribute("data-offline", "false");
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Mangel melden" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("kiosk-zustand")).toHaveAttribute(
|
||||
"data-offline",
|
||||
"false",
|
||||
);
|
||||
|
||||
// --- Netz weg -----------------------------------------------------------
|
||||
await context.setOffline(true);
|
||||
|
||||
// Die Zustandsleiste sagt es, ohne dass jemand danach suchen muss.
|
||||
await expect(page.getByTestId("kiosk-zustand")).toHaveAttribute("data-offline", "true");
|
||||
await expect(page.getByTestId("kiosk-zustand")).toHaveAttribute(
|
||||
"data-offline",
|
||||
"true",
|
||||
);
|
||||
await expect(page.getByText("Kein Netz")).toBeVisible();
|
||||
|
||||
// Trotzdem erfassen: Nummer eintippen (eine Kamera gibt es im Test nicht),
|
||||
@@ -164,28 +186,41 @@ test.describe("Offline-Betrieb", () => {
|
||||
// Und der Vorgang wartet sichtbar in der Warteschlange. Die Bestätigung
|
||||
// springt nach ein paar Sekunden zurück auf die Kacheln; die Leiste läuft
|
||||
// dort weiter, deshalb hält die Zusicherung über den Wechsel hinweg.
|
||||
await expect(page.getByTestId("kiosk-zustand")).toHaveAttribute("data-offen", "1");
|
||||
await expect(page.getByTestId("kiosk-zustand")).toHaveAttribute(
|
||||
"data-offen",
|
||||
"1",
|
||||
);
|
||||
await expect(page.getByText("Änderung wartet")).toBeVisible();
|
||||
|
||||
// --- Netz zurück --------------------------------------------------------
|
||||
await context.setOffline(false);
|
||||
|
||||
await expect(page.getByTestId("kiosk-zustand")).toHaveAttribute("data-offline", "false", {
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(page.getByTestId("kiosk-zustand")).toHaveAttribute(
|
||||
"data-offline",
|
||||
"false",
|
||||
{
|
||||
timeout: 30_000,
|
||||
},
|
||||
);
|
||||
|
||||
// Die Warteschlange leert sich von selbst — der Knopf ist nur die Abkürzung
|
||||
// für den Ungeduldigen und darf deshalb auch schon verschwunden sein.
|
||||
const uebertragen = page.getByRole("button", { name: "Jetzt übertragen" });
|
||||
if (await uebertragen.isVisible()) await uebertragen.click();
|
||||
|
||||
await expect(page.getByTestId("kiosk-zustand")).toHaveAttribute("data-offen", "0", {
|
||||
timeout: 60_000,
|
||||
});
|
||||
await expect(page.getByTestId("kiosk-zustand")).toHaveAttribute(
|
||||
"data-offen",
|
||||
"0",
|
||||
{
|
||||
timeout: 60_000,
|
||||
},
|
||||
);
|
||||
await expect(page.getByText("alles übertragen")).toBeVisible();
|
||||
});
|
||||
|
||||
test("der offline erfasste Mangel steht danach in der Verwaltung", async ({ page }) => {
|
||||
test("der offline erfasste Mangel steht danach in der Verwaltung", async ({
|
||||
page,
|
||||
}) => {
|
||||
expect(mangeltext, "der vorherige Test muss gelaufen sein").not.toBe("");
|
||||
|
||||
await verwaltungAnmelden(page);
|
||||
@@ -206,7 +241,9 @@ test.describe("Offline-Betrieb", () => {
|
||||
await terminalAnmelden(page);
|
||||
|
||||
await page.goto("/kiosk/waesche");
|
||||
await expect(page.getByRole("heading", { name: "Wäsche abgeben" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Wäsche abgeben" }),
|
||||
).toBeVisible();
|
||||
|
||||
/**
|
||||
* Der Fall, für den es den Idempotenzschlüssel gibt: die Anfrage kommt beim
|
||||
@@ -221,7 +258,10 @@ test.describe("Offline-Betrieb", () => {
|
||||
let ersteAntwortVerschluckt = false;
|
||||
await context.route("**/api/trpc/**", async (route) => {
|
||||
const adresse = route.request().url();
|
||||
if (!ersteAntwortVerschluckt && adresse.includes("kiosk.vorgaengeSenden")) {
|
||||
if (
|
||||
!ersteAntwortVerschluckt &&
|
||||
adresse.includes("kiosk.vorgaengeSenden")
|
||||
) {
|
||||
ersteAntwortVerschluckt = true;
|
||||
// Anfrage geht durch (der Server bucht), die Antwort kommt nie an.
|
||||
await route.fetch();
|
||||
@@ -247,14 +287,19 @@ test.describe("Offline-Betrieb", () => {
|
||||
await page.getByRole("button", { name: "Jetzt übertragen" }).click();
|
||||
|
||||
// Der zweite Versuch läuft durch — der Vorgang ist damit erledigt.
|
||||
await expect(page.getByTestId("kiosk-zustand")).toHaveAttribute("data-offen", "0", {
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
expect(ersteAntwortVerschluckt, "die erste Übertragung muss unterbrochen worden sein").toBe(
|
||||
true,
|
||||
await expect(page.getByTestId("kiosk-zustand")).toHaveAttribute(
|
||||
"data-offen",
|
||||
"0",
|
||||
{
|
||||
timeout: 60_000,
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
ersteAntwortVerschluckt,
|
||||
"die erste Übertragung muss unterbrochen worden sein",
|
||||
).toBe(true);
|
||||
|
||||
// Erst abräumen, wenn nichts mehr unterwegs ist — sonst schlägt eine noch
|
||||
// laufende Umleitung beim Testende zu.
|
||||
await context.unrouteAll({ behavior: "ignoreErrors" });
|
||||
@@ -275,13 +320,19 @@ test.describe("Offline-Betrieb", () => {
|
||||
});
|
||||
|
||||
test.describe("Hallenmonitor", () => {
|
||||
test("zeigt Uhrzeit, Anwesende, Fahrzeuge und fällige Prüfungen", async ({ page }) => {
|
||||
test("zeigt Uhrzeit, Anwesende, Fahrzeuge und fällige Prüfungen", async ({
|
||||
page,
|
||||
}) => {
|
||||
await terminalAnmelden(page);
|
||||
await page.goto("/kiosk/monitor");
|
||||
|
||||
await expect(page.getByText("Im Haus")).toBeVisible();
|
||||
await expect(page.getByText("Fahrzeuge", { exact: true }).first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Fahrzeuge", { exact: true }).first(),
|
||||
).toBeVisible();
|
||||
// Die Uhr läuft — das Format genügt als Nachweis.
|
||||
await expect(page.getByText(/^\d{2}:\d{2}(:\d{2})?$/).first()).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/^\d{2}:\d{2}(:\d{2})?$/).first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user