import { expect, test, type Page } from "@playwright/test"; import "dotenv/config"; import { prisma } from "@/lib/db"; /** * 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`. */ 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"]')]); } /** * 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 { const stempel = Date.now(); const kunde = await prisma.kunde.findFirstOrThrow({ select: { id: true } }); const vorlage = await prisma.vorlage.create({ data: { name: `ZZ E2E Druck ${stempel}`, 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: `ZZ E2E Druck ${stempel}` }, }); return lauf.id; } 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 }) => { await anmelden(page); const zuletzt = page .locator("section") .filter({ has: page.getByRole("heading", { name: "Zuletzt bearbeitet" }) }); await zuletzt.getByRole("link").first().click(); await page.waitForURL(/\/laeufe\/[^/]+$/); }); 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."); await anmelden(page); const lauf = await prisma.lauf.findFirstOrThrow({ select: { id: true } }); 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 laufId = await laufMitPasswortschritt(); await anmelden(page); await page.goto(`/laeufe/${laufId}`); 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: "ZZ E2E Druck" } }, 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 } } }); } await prisma.vorlage.deleteMany({ where: { name: { startsWith: "ZZ E2E Druck" } } }); await prisma.$disconnect(); });