Files
Florian WildeandClaude Opus 5 7196c7110c 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>
2026-09-04 13:24:48 +02:00

265 lines
8.7 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { anmelden, eigenerName } from "./wehr";
/**
* E2E-Tests für Phase 8 — Wissen, Belege und die KI-Protokolle.
*
* **Ohne laufendes Sprachmodell.** Das ist Absicht und nicht eine Lücke: der
* Testlauf hat keinen Ollama daneben, und genau so sieht es bei einer Wehr
* aus, die FLORIAN aufsetzt, bevor sie sich um Modelle kümmert. Geprüft wird
* deshalb vor allem, dass die Anwendung ohne Modell **vollständig benutzbar**
* bleibt und ehrlich sagt, was fehlt.
*
* Was ohne Modell laufen muss:
*
* * Wiki lesen, anlegen, ändern, Fassungen ansehen
* * Wortsuche
* * Belege fotografieren und von Hand ausfüllen
* * beide Protokolle
*
* Voraussetzung: Stack läuft, Datenbank ist migriert und geseedet.
*/
test.describe.configure({ mode: "serial" });
test.describe("Wissen", () => {
test("zeigt die Seiten aus dem Bestand", async ({ page }) => {
await anmelden(page);
await page.goto("/wiki");
await expect(
page.getByRole("heading", { name: "Wissen", level: 1 }),
).toBeVisible();
// Aus dem Seed.
await expect(
page.getByRole("link", { name: "Prüffristen im Überblick" }),
).toBeVisible();
await expect(
page.getByRole("link", {
name: "Atemschutz — was vor jedem Einsatz gilt",
}),
).toBeVisible();
});
/**
* Der wichtigste Test der Datei: ohne Modell muss dastehen, dass keines da
* ist — und im selben Atemzug, dass die Wortsuche trotzdem funktioniert.
*/
test("sagt ohne Sprachmodell, was fehlt und was trotzdem geht", async ({
page,
}) => {
await anmelden(page);
await page.goto("/wiki");
await expect(
page.getByText("Es ist kein Sprachmodell eingerichtet."),
).toBeVisible();
await expect(page.getByText(/Wortsuche/)).toBeVisible();
});
test("findet eine Textstelle über die Wortsuche", async ({ page }) => {
await anmelden(page);
await page.goto("/wiki");
await page.getByLabel("Wortsuche im Wiki").fill("Rückzugsdruck");
await expect(page.getByText(/Wortsuche: \d+ Stelle/)).toBeVisible();
// Der Ausschnitt zeigt die Textstelle mitsamt ihrer Umgebung, nicht bloß
// den Seitennamen — daran erkennt man beim Suchen, ob es die richtige
// Stelle ist, ohne die Seite zu öffnen.
await expect(
page.getByText(/Der Rückzugsdruck ist der Druck/).first(),
).toBeVisible();
});
test("zeigt eine Seite als formatierten Text", async ({ page }) => {
await anmelden(page);
await page.goto("/wiki/atemschutz-merkblatt");
await expect(
page.getByRole("heading", {
name: "Atemschutz — was vor jedem Einsatz gilt",
level: 1,
}),
).toBeVisible();
// Markdown wird als echte Überschrift dargestellt, nicht als Raute im Text.
await expect(
page.getByRole("heading", { name: "Rückzugsdruck" }),
).toBeVisible();
await expect(page.getByText(/FwDV 7, Ausgabe 2002/)).toBeVisible();
});
test("stellt eine Markdown-Tabelle als Tabelle dar", async ({ page }) => {
await anmelden(page);
await page.goto("/wiki/schlaeuche-pflegen");
await expect(
page.getByRole("columnheader", { name: "Prüfdruck" }),
).toBeVisible();
await expect(page.getByRole("cell", { name: "C 42 / C 52" })).toBeVisible();
});
/**
* Ohne Modell gibt es keine Einbettungen — die Seite sagt das, statt so zu
* tun, als wäre sie durchsuchbar.
*/
test("weist auf den fehlenden Suchstand hin", async ({ page }) => {
await anmelden(page);
await page.goto("/wiki/alarmierungsordnung");
await expect(
page.getByText(/Suchstand ist älter als der Text/),
).toBeVisible();
});
test("legt eine Seite an und zeigt sie sofort", async ({ page }) => {
const titel = eigenerName("Merkblatt");
await anmelden(page);
await page.goto("/wiki");
await page.getByRole("button", { name: "Seite anlegen" }).click();
await page.getByLabel("Titel").fill(titel);
await page
.getByLabel("Inhalt")
.fill("## Erster Abschnitt\n\nHier steht der Text.");
await page.getByRole("button", { name: "Anlegen" }).click();
await expect(
page.getByRole("heading", { name: titel, level: 1 }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Erster Abschnitt" }),
).toBeVisible();
});
/**
* Beim Ändern wird die alte Fassung weggeschrieben. Eine Dienstvorschrift,
* die sich unbemerkt ändert, ist schlimmer als keine.
*/
test("hebt die vorherige Fassung auf", async ({ page }) => {
const titel = eigenerName("Fassungen");
await anmelden(page);
await page.goto("/wiki");
await page.getByRole("button", { name: "Seite anlegen" }).click();
await page.getByLabel("Titel").fill(titel);
await page.getByLabel("Inhalt").fill("Der erste Text.");
await page.getByRole("button", { name: "Anlegen" }).click();
await expect(
page.getByRole("heading", { name: titel, level: 1 }),
).toBeVisible();
await page.getByRole("button", { name: "Bearbeiten" }).click();
const dialog = page.getByRole("dialog");
await dialog.getByLabel("Inhalt").fill("Der zweite Text.");
await dialog
.getByLabel("Was wurde geändert? (freiwillig)")
.fill("Text berichtigt");
await dialog.getByRole("button", { name: "Speichern" }).click();
await expect(page.getByText("Der zweite Text.")).toBeVisible();
await page.getByRole("button", { name: "Verlauf" }).click();
const verlauf = page.getByRole("dialog");
await expect(verlauf.getByText("Text berichtigt")).toBeVisible();
await verlauf.getByRole("button", { name: /Fassung 1/ }).click();
await expect(verlauf.getByText("Der erste Text.")).toBeVisible();
});
test("zeigt beim Bearbeiten, in wie viele Abschnitte zerlegt wird", async ({
page,
}) => {
await anmelden(page);
await page.goto("/wiki/pruefristen-im-ueberblick");
await page.getByRole("button", { name: "Bearbeiten" }).click();
await expect(
page.getByText(/Wird für die Suche in \d+ Abschnitte zerlegt/),
).toBeVisible();
});
test("ein Entwurf wird als solcher gekennzeichnet", async ({ page }) => {
const titel = eigenerName("Entwurf");
await anmelden(page);
await page.goto("/wiki");
await page.getByRole("button", { name: "Seite anlegen" }).click();
const dialog = page.getByRole("dialog");
await dialog.getByLabel("Titel").fill(titel);
await dialog.getByLabel("Inhalt").fill("Noch nicht fertig.");
// Das Kästchen selbst anklicken, nicht seine Beschriftung: auf dem Tablet
// trifft ein Klick auf den umbrochenen Text gelegentlich daneben.
await dialog.getByRole("checkbox").uncheck();
await dialog.getByRole("button", { name: "Anlegen" }).click();
await expect(page.getByText(/Diese Seite ist ein/)).toBeVisible();
});
});
test.describe("Belege", () => {
test("sagt ohne Vision-Modell, was fehlt und was trotzdem geht", async ({
page,
}) => {
await anmelden(page);
await page.goto("/belege");
await expect(
page.getByRole("heading", { name: "Belege", level: 1 }),
).toBeVisible();
await expect(
page.getByText(/kein Modell eingerichtet, das Bilder lesen kann/),
).toBeVisible();
await expect(page.getByText(/von Hand eingetragen/)).toBeVisible();
await expect(
page.getByRole("button", { name: "Beleg fotografieren" }),
).toBeVisible();
});
});
test.describe("Protokolle", () => {
test("zeigt das KI-Protokoll mit seinen Kennzahlen", async ({ page }) => {
await anmelden(page);
await page.goto("/ki-protokoll");
await expect(
page.getByRole("heading", { name: "KI-Protokoll", level: 1 }),
).toBeVisible();
await expect(page.getByText("Vorgänge insgesamt")).toBeVisible();
await expect(
page.getByText("erzeugt, aber nicht entschieden"),
).toBeVisible();
// Die Zusicherung, die den Zweck der Liste ausmacht.
await expect(
page.getByText(/lassen sich nachträglich nicht ändern/),
).toBeVisible();
});
test("zeigt das Audit-Log", async ({ page }) => {
await anmelden(page);
await page.goto("/protokoll");
await expect(
page.getByRole("heading", { name: "Protokoll", level: 1 }),
).toBeVisible();
await expect(page.getByText(/append-only/)).toBeVisible();
// Die Anmeldung von eben steht schon drin.
await expect(page.getByText("angemeldet").first()).toBeVisible();
});
test("filtert das Audit-Log nach Art des Datensatzes", async ({ page }) => {
await anmelden(page);
await page.goto("/protokoll");
await page.getByLabel("Nach Entität filtern").fill("Wikiseite");
await expect(page.getByText(/Wiki-Seite/).first()).toBeVisible();
});
});