Files
Laufzettel/tests/e2e/uebersicht-druck.spec.ts
T
florian.wilde fbbb634e56
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(doku): Anleitung, PDF und Ende-zu-Ende-Abnahme (#36)
2026-09-26 09:04:15 +00:00

183 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
});