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(); }); });