feat(doku): Anleitung, PDF und Ende-zu-Ende-Abnahme
Neun Kapitel unter docs/anleitung/ fuer Technik und Verwaltung, mit 35 Bildern aus `pnpm docs:screenshots` und einem Fehlerbehebungskapitel aus echten Meldungen. `pnpm docs:pdf` macht daraus 72 Seiten A4 — hell gedruckt, mit Deckblatt, Inhaltsverzeichnis und Seitenzahlen, ohne neue Abhaengigkeit. Die Ablaeufe pruefen ihre Voraussetzungen, bevor der erste startet: antwortet ein gebauter Stand, steht der Seed-Bestand, und liest der Server dieselbe Datenbank wie der Testprozess. Jeder Ablauf legt an, was er braucht, und raeumt es weg. Vier neue kommen dazu: Hauptablauf, Onboarding mit Vergleich der Zwischenablage, Rollen und 390 px auf achtzehn Seiten. Drei Fehler kamen dabei heraus. Das Projekt „handy" pruefte 412 px statt der verlangten 390. Ein angemeldeter Benutzer ohne Recht erfuhr nie, warum er abgewiesen wurde — die Meldung stand auf einer Seite, die ihn sofort weiterleitet; jetzt gibt es /kein-zugriff. Und der Verzeichnis-Ablauf schrieb die Attributzuordnung der ersten vorhandenen Quelle um, in einer echten Installation also der produktiven. 101 Ablaeufe in INTERN und 97 in DIENSTLEISTUNG, beide ohne Fehlschlag. Closes #18 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9bd198a531
commit
10e272654f
+41
-21
@@ -1,6 +1,7 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import "dotenv/config";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { anmelden, SEED_VORLAGE } from "./helfer";
|
||||
|
||||
/**
|
||||
* Das Archiv am gebauten Stand (#015).
|
||||
@@ -9,20 +10,18 @@ import { prisma } from "@/lib/db";
|
||||
* Mandantengrenze hinein. Hier geht es um den Weg, den ein Mensch nimmt: Wort
|
||||
* eintippen, Treffer sehen, **erkennen warum**, in den Lauf springen. Ein
|
||||
* Treffer ohne sichtbaren Grund ist fuer den Suchenden wertlos.
|
||||
*
|
||||
* Voraussetzungen prueft `tests/e2e/global-setup.ts`, aufgeraeumt wird am Ende
|
||||
* ueber die Datenbank: fuer einen Lauf kennt die Anwendung keinen Loeschweg, und
|
||||
* das ist richtig so (#018).
|
||||
*/
|
||||
|
||||
const STEMPEL = Date.now();
|
||||
const NOTIZ = `Restposten ${STEMPEL} aus dem Lager`;
|
||||
|
||||
async function anmelden(seite: Page) {
|
||||
const email = process.env["SEED_ADMIN_EMAIL"];
|
||||
const passwort = process.env["SEED_ADMIN_PASSWORD"];
|
||||
if (!email || !passwort) throw new Error("SEED_ADMIN_EMAIL und SEED_ADMIN_PASSWORD fehlen.");
|
||||
await seite.goto("/anmelden");
|
||||
await seite.fill('input[name="email"]', email);
|
||||
await seite.fill('input[name="passwort"]', passwort);
|
||||
await Promise.all([seite.waitForURL("**/"), seite.click('button[type="submit"]')]);
|
||||
}
|
||||
/** Fortlaufend, damit zwei Laeufe desselben Durchgangs sich nicht verwechseln. */
|
||||
let laufende = 0;
|
||||
|
||||
type Testlauf = { readonly id: string; readonly titel: string; readonly suchwort: string };
|
||||
|
||||
/**
|
||||
* Ein archivierter Lauf mit einer Notiz, die es sonst nirgends gibt.
|
||||
@@ -31,19 +30,31 @@ async function anmelden(seite: Page) {
|
||||
* geprueft werden, nicht der Weg dorthin — den prueft `tests/e2e/laeufe.spec.ts`
|
||||
* vom Anlegen bis zum Archivieren. Die Zeitstempel gehoeren trotzdem dazu, drei
|
||||
* CHECK-Bedingungen in der Datenbank verlangen sie.
|
||||
*
|
||||
* Vorlage **am Namen**, Kunde ueber `istEigeneOrganisation`: ein
|
||||
* `findFirstOrThrow` ohne Bedingung nahm die Version, die die Sortierung gerade
|
||||
* nach vorn stellte, und im Modus DIENSTLEISTUNG einen beliebigen Demo-Kunden.
|
||||
* Dass es `SEED_VORLAGE` veroeffentlicht gibt, prueft der Global-Setup.
|
||||
*/
|
||||
async function archivierterLauf(): Promise<{ id: string; titel: string }> {
|
||||
async function archivierterLauf(): Promise<Testlauf> {
|
||||
laufende += 1;
|
||||
const marke = `${STEMPEL}-${laufende}`;
|
||||
const suchwort = `Restposten ${marke}`;
|
||||
|
||||
const version = await prisma.vorlagenVersion.findFirstOrThrow({
|
||||
where: { status: "VEROEFFENTLICHT" },
|
||||
orderBy: { veroeffentlichtAm: "desc" },
|
||||
where: { status: "VEROEFFENTLICHT", vorlage: { name: SEED_VORLAGE } },
|
||||
orderBy: { version: "desc" },
|
||||
select: { id: true, abschnitte: { select: { schritte: { select: { id: true } } } } },
|
||||
});
|
||||
const kunde = await prisma.kunde.findFirstOrThrow({ select: { id: true } });
|
||||
const kunde = await prisma.kunde.findFirstOrThrow({
|
||||
where: { istEigeneOrganisation: true },
|
||||
select: { id: true },
|
||||
});
|
||||
const schrittId = version.abschnitte.flatMap((abschnitt) => abschnitt.schritte)[0]?.id;
|
||||
if (!schrittId) throw new Error("Die Vorlage hat keinen Schritt.");
|
||||
if (!schrittId) throw new Error(`Die Vorlage „${SEED_VORLAGE}“ hat keinen Schritt.`);
|
||||
|
||||
const jetzt = new Date();
|
||||
const titel = `ZZ E2E Archiv ${STEMPEL}`;
|
||||
const titel = `ZZ E2E Archiv ${marke}`;
|
||||
const lauf = await prisma.lauf.create({
|
||||
data: {
|
||||
vorlagenVersionId: version.id,
|
||||
@@ -60,10 +71,10 @@ async function archivierterLauf(): Promise<{ id: string; titel: string }> {
|
||||
laufId: lauf.id,
|
||||
vorlagenSchrittId: schrittId,
|
||||
zustand: "NICHT_ZUTREFFEND",
|
||||
notiz: NOTIZ,
|
||||
notiz: `${suchwort} aus dem Lager`,
|
||||
},
|
||||
});
|
||||
return { id: lauf.id, titel };
|
||||
return { id: lauf.id, titel, suchwort };
|
||||
}
|
||||
|
||||
test.describe("Archiv", () => {
|
||||
@@ -77,7 +88,7 @@ test.describe("Archiv", () => {
|
||||
}
|
||||
await page.getByRole("link", { name: "Archiv" }).first().click();
|
||||
await page.waitForURL("**/archiv");
|
||||
await page.getByLabel("Suchbegriff").fill(`Restposten ${STEMPEL}`);
|
||||
await page.getByLabel("Suchbegriff").fill(lauf.suchwort);
|
||||
await page.getByRole("button", { name: "Suchen" }).click();
|
||||
|
||||
const treffer = page.getByRole("listitem").filter({ hasText: lauf.titel });
|
||||
@@ -102,8 +113,16 @@ test.describe("Archiv", () => {
|
||||
|
||||
test("trägt am Handy ohne horizontales Scrollen", async ({ page, isMobile }) => {
|
||||
test.skip(!isMobile, "Nur im Projekt „handy“.");
|
||||
/*
|
||||
* Der Ablauf bringt seinen Treffer mit, statt auf einen zu hoffen: gegen
|
||||
* eine frische Datenbank fand „Restposten“ nichts, und gemessen wurde dann
|
||||
* die Breite einer Seite ohne Treffer (#018).
|
||||
*/
|
||||
const lauf = await archivierterLauf();
|
||||
await anmelden(page);
|
||||
await page.goto("/archiv?suche=Restposten");
|
||||
await page.goto(`/archiv?suche=${encodeURIComponent(lauf.suchwort)}`);
|
||||
await expect(page.getByRole("listitem").filter({ hasText: lauf.titel })).toHaveCount(1);
|
||||
|
||||
const masse = await page.evaluate(() => ({
|
||||
scroll: document.documentElement.scrollWidth,
|
||||
sichtbar: document.documentElement.clientWidth,
|
||||
@@ -114,6 +133,7 @@ test.describe("Archiv", () => {
|
||||
|
||||
test.afterAll(async () => {
|
||||
const laeufe = await prisma.lauf.findMany({
|
||||
// Der Praefix gehoert dieser Datei allein – geraeumt wird nur, was sie anlegt.
|
||||
where: { titel: { startsWith: "ZZ E2E Archiv" } },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user