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>
183 lines
7.6 KiB
TypeScript
183 lines
7.6 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
import "dotenv/config";
|
||
import { prisma } from "@/lib/db";
|
||
import { anmelden } from "./helfer";
|
||
|
||
/**
|
||
* Uebersicht und Druckansicht am gebauten Stand (#015).
|
||
*
|
||
* Beide Seiten leben von ihrer Darstellung: die Uebersicht sortiert nach
|
||
* Dringlichkeit, die Druckansicht gehoert auf Papier. Was davon stimmt, zeigt
|
||
* erst der Browser — die Lesemodelle dahinter pruefen `tests/uebersicht.test.ts`
|
||
* und `tests/druckansicht.test.ts`.
|
||
*
|
||
* Voraussetzungen prueft `tests/e2e/global-setup.ts`. Jeden Lauf, den diese
|
||
* Ablaeufe brauchen, legen sie selbst an und raeumen ihn hinterher weg. Vorher
|
||
* holten sie sich mit `findFirstOrThrow` irgendeinen vorhandenen Lauf und
|
||
* erwarteten, dass „Zuletzt bearbeitet“ gefuellt ist — gegen eine frische
|
||
* Datenbank scheiterte beides, das eine mit „No record was found“, das andere
|
||
* mit einem Zeitablauf auf einen Verweis, den es nicht gab (#018).
|
||
*/
|
||
|
||
const STEMPEL = Date.now();
|
||
|
||
/** Fortlaufend, damit die Laeufe eines Durchgangs sich nicht verwechseln. */
|
||
let laufende = 0;
|
||
|
||
/** Name und Titel tragen diesen Praefix – nur daran raeumt der Nachlauf auf. */
|
||
const PRAEFIX = "ZZ E2E Druck";
|
||
|
||
/**
|
||
* Ein Lauf aus einer Vorlage, die `{{generiertes_passwort}}` wirklich enthaelt.
|
||
*
|
||
* Direkt ueber Prisma: geprueft werden soll die **Druckansicht**, nicht der Weg
|
||
* zum Lauf — den prueft `tests/e2e/laeufe.spec.ts` vom Anlegen bis zum Archiv.
|
||
*/
|
||
async function laufMitPasswortschritt(): Promise<{ id: string; titel: string }> {
|
||
laufende += 1;
|
||
const marke = `${PRAEFIX} ${STEMPEL}-${laufende}`;
|
||
// Ueber `istEigeneOrganisation` und nicht ueber den ersten Treffer: im Modus
|
||
// DIENSTLEISTUNG waere das ein beliebiger Demo-Kunde.
|
||
const kunde = await prisma.kunde.findFirstOrThrow({
|
||
where: { istEigeneOrganisation: true },
|
||
select: { id: true },
|
||
});
|
||
const vorlage = await prisma.vorlage.create({ data: { name: marke, kategorie: "ZZ Test" } });
|
||
const version = await prisma.vorlagenVersion.create({
|
||
data: { vorlageId: vorlage.id, version: 1, status: "ENTWURF" },
|
||
});
|
||
const abschnitt = await prisma.vorlagenAbschnitt.create({
|
||
data: { versionId: version.id, titel: "Konto" },
|
||
});
|
||
await prisma.vorlagenSchritt.create({
|
||
data: {
|
||
abschnittId: abschnitt.id,
|
||
titel: "Konto angelegt",
|
||
typ: "BEFEHL",
|
||
konfig: { shell: "powershell", befehl: 'New-ADUser -Password "{{generiertes_passwort}}"' },
|
||
},
|
||
});
|
||
await prisma.vorlagenVersion.update({
|
||
where: { id: version.id },
|
||
data: { status: "VEROEFFENTLICHT", veroeffentlichtAm: new Date() },
|
||
});
|
||
|
||
const lauf = await prisma.lauf.create({
|
||
data: { vorlagenVersionId: version.id, kundeId: kunde.id, titel: marke },
|
||
});
|
||
return { id: lauf.id, titel: marke };
|
||
}
|
||
|
||
test.describe("Übersicht", () => {
|
||
test("zeigt die vier Blöcke in ihrer Reihenfolge", async ({ page }) => {
|
||
await anmelden(page);
|
||
const ueberschriften = await page.getByRole("heading", { level: 2 }).allInnerTexts();
|
||
expect(ueberschriften).toEqual(["Überfällig", "Mir zugewiesen", "Zuletzt bearbeitet", "Schnellstart"]);
|
||
});
|
||
|
||
test("führt von einer Zeile in den Lauf", async ({ page }) => {
|
||
/*
|
||
* Der Ablauf bringt die Zeile mit, auf die er klickt. „Zuletzt bearbeitet“
|
||
* sortiert nach `geaendertAm` absteigend, der eben angelegte Lauf steht also
|
||
* oben — und weil der Klick auf **seinen** Titel geht, prueft der Test einen
|
||
* Weg und nicht die Datenlage.
|
||
*/
|
||
const lauf = await laufMitPasswortschritt();
|
||
await anmelden(page);
|
||
const zuletzt = page
|
||
.locator("section")
|
||
.filter({ has: page.getByRole("heading", { name: "Zuletzt bearbeitet" }) });
|
||
await zuletzt.getByRole("link", { name: new RegExp(lauf.titel) }).click();
|
||
await page.waitForURL(`**/laeufe/${lauf.id}`);
|
||
});
|
||
|
||
test("trägt am Handy ohne horizontales Scrollen", async ({ page, isMobile }) => {
|
||
test.skip(!isMobile, "Nur im Projekt „handy“.");
|
||
await anmelden(page);
|
||
const masse = await page.evaluate(() => ({
|
||
scroll: document.documentElement.scrollWidth,
|
||
sichtbar: document.documentElement.clientWidth,
|
||
}));
|
||
expect(masse.scroll).toBeLessThanOrEqual(masse.sichtbar);
|
||
});
|
||
});
|
||
|
||
test.describe("Druckansicht", () => {
|
||
test("ist hell, ohne Navigation und passt auf A4", async ({ page }) => {
|
||
test.skip(test.info().project.name !== "desktop", "Das Seitenformat hängt nicht am Gerät.");
|
||
// Der eigene Lauf statt `findFirstOrThrow`: gegen eine frische Datenbank gab
|
||
// es keinen, und die Meldung sprach von „No record was found“ (#018).
|
||
const lauf = await laufMitPasswortschritt();
|
||
await anmelden(page);
|
||
await page.goto(`/drucken/lauf/${lauf.id}`);
|
||
|
||
// Keine Hülle: weder Seitenleiste noch der Scannen-Knopf gehören aufs Blatt.
|
||
await expect(page.getByRole("navigation", { name: "Hauptnavigation" })).toHaveCount(0);
|
||
await expect(page.getByRole("link", { name: "Scannen" })).toHaveCount(0);
|
||
|
||
// Hell heisst hell: der Grund des Belegs ist keine dunkle Fläche.
|
||
const grund = await page
|
||
.locator(".druck-hell")
|
||
.first()
|
||
.evaluate((element) => getComputedStyle(element).backgroundColor);
|
||
expect(grund).toBe("rgb(255, 255, 255)");
|
||
|
||
/*
|
||
* A4 kommt aus `@page`, und nur ein echtes PDF zeigt, ob die Regel greift:
|
||
* 210 × 297 mm sind 595 × 842 Punkte.
|
||
*/
|
||
const pdf = await page.pdf({ preferCSSPageSize: true });
|
||
const text = pdf.toString("latin1");
|
||
const treffer = /\/MediaBox\s*\[\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\]/u.exec(text);
|
||
expect(treffer).not.toBeNull();
|
||
expect(Number(treffer?.[3]) - Number(treffer?.[1])).toBeCloseTo(595, 0);
|
||
expect(Number(treffer?.[4]) - Number(treffer?.[2])).toBeCloseTo(842, 0);
|
||
});
|
||
|
||
test("ist vom Lauf aus erreichbar und erzeugt kein Passwort", async ({ page }) => {
|
||
/*
|
||
* Der Lauf bringt seine eigene Vorlage mit — eine **mit**
|
||
* `{{generiertes_passwort}}` im Befehl. Vorher lief dieser Test gegen
|
||
* NB-001, dessen Vorlage den Platzhalter gar nicht enthält: er hätte auch
|
||
* dann bestanden, wenn die Druckansicht Passwörter erzeugte (Reviewbefund
|
||
* zu #015).
|
||
*/
|
||
const lauf = await laufMitPasswortschritt();
|
||
await anmelden(page);
|
||
await page.goto(`/laeufe/${lauf.id}`);
|
||
await page.getByRole("link", { name: "Drucken" }).click();
|
||
await page.waitForURL(/\/drucken\/lauf\//);
|
||
|
||
const blatt = await page.locator("body").innerText();
|
||
expect(blatt).toContain("[Passwort – nicht gespeichert]");
|
||
expect(blatt).not.toContain("generiertes_passwort");
|
||
|
||
/*
|
||
* Und der zweite Beweis, der ohne Kenntnis des Textes auskommt: ein
|
||
* erzeugtes Passwort wäre bei jedem Aufbau ein anderes. Sind beide Blätter
|
||
* Zeichen für Zeichen gleich, kann keines darin stecken.
|
||
*/
|
||
await page.reload();
|
||
const nochmal = await page.locator("body").innerText();
|
||
expect(nochmal.replace(/Gedruckt am[^\n]*/u, "")).toBe(blatt.replace(/Gedruckt am[^\n]*/u, ""));
|
||
});
|
||
});
|
||
|
||
test.afterAll(async () => {
|
||
const laeufe = await prisma.lauf.findMany({
|
||
where: { titel: { startsWith: PRAEFIX } },
|
||
select: { id: true },
|
||
});
|
||
const kennungen = laeufe.map((lauf) => lauf.id);
|
||
if (kennungen.length > 0) {
|
||
await prisma.schrittergebnis.deleteMany({ where: { laufId: { in: kennungen } } });
|
||
await prisma.protokoll.deleteMany({
|
||
where: { OR: [{ laufId: { in: kennungen } }, { entitaetId: { in: kennungen } }] },
|
||
});
|
||
await prisma.lauf.deleteMany({ where: { id: { in: kennungen } } });
|
||
}
|
||
// Die Vorlagen zuletzt: an ihnen hingen gerade noch die Laeufe.
|
||
await prisma.vorlage.deleteMany({ where: { name: { startsWith: PRAEFIX } } });
|
||
await prisma.$disconnect();
|
||
});
|