import { expect, test, type Page } from "@playwright/test"; import "dotenv/config"; import { prisma } from "@/lib/db"; /** * Der Weg vom Aufkleber zur Arbeit (#014). * * Das ist der Ablauf, fuer den die Anwendung gebaut ist: scannen, sehen was zu * tun ist, loslegen. Geprueft wird er am **gebauten** Stand, weil genau hier die * Teile zusammenkommen, die einzeln schon funktionierten — die Weiche unter * `/t/`, das Registrieren, die Geraeteseite und der Lauf. */ const STEMPEL = Date.now(); 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"]')]); } /** * Eine schlichte Vorlage **ohne** Pflichtangaben. * * Die Beispielvorlage verlangt Betriebssystem und Einbindung; „Vorlage zuweisen“ * fragt beides nicht ab und fuehrt dafuer in den Start-Dialog. Dieser Test will * aber den kurzen Weg pruefen, also bringt er seine eigene Vorlage mit. */ async function vorlageAnlegen(): Promise { const name = `ZZ E2E Geraetelauf ${STEMPEL}`; const vorlage = await prisma.vorlage.create({ data: { name, 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: "Prüfen" }, }); await prisma.vorlagenSchritt.create({ data: { abschnittId: abschnitt.id, titel: "Gerät angesehen", pflicht: true }, }); await prisma.vorlagenVersion.update({ where: { id: version.id }, data: { status: "VEROEFFENTLICHT", veroeffentlichtAm: new Date() }, }); return name; } /** Die Tags, die dieser Lauf angelegt hat – nur die raeumt er hinterher weg. */ const eigeneTags: string[] = []; /** Legt einen vorgedruckten Tag an – ueber den Dienst, nicht ueber die Oberflaeche. */ async function tagAnlegen(): Promise { const { tagsErzeugen } = await import("@/server/services/tags"); const admin = await prisma.benutzer.findFirstOrThrow({ where: { rolle: "ADMIN" }, select: { id: true, rolle: true }, }); const stapel = await tagsErzeugen(1, null, { id: admin.id, rolle: admin.rolle }); const code = stapel.tagCodes[0]; if (!code) throw new Error("Der Stapel kam leer zurück."); eigeneTags.push(code); return code; } test.describe("Vom Aufkleber zur Arbeit", () => { test("führt einen unbekannten Code nicht ins Leere", async ({ page }) => { await anmelden(page); await page.goto("/t/LZ-ZZZZZZ"); await expect(page.getByRole("heading", { name: "Kein Laufzettel-Tag" })).toBeVisible(); await page.getByRole("link", { name: "Gerät suchen" }).click(); await page.waitForURL("**/geraete"); }); test("schickt einen nicht angemeldeten Scan über die Anmeldung ans Ziel", async ({ page }) => { /* * Das erste Akzeptanzkriterium des Auftrags, und der Fall, den ein Techniker * am häufigsten hat: das Handy scannt, die Sitzung ist abgelaufen. Danach * muss er auf **der Geräteseite** stehen und nicht auf der Übersicht — sonst * sucht er das Gerät noch einmal, das er gerade in der Hand hält. */ const geraet = await prisma.geraet.findFirstOrThrow({ where: { hostname: "PC-001" }, select: { id: true, tagCode: true }, }); await page.context().clearCookies(); await page.goto(`/t/${geraet.tagCode}`); await page.waitForURL(/\/anmelden\?ziel=/); 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 page.fill('input[name="email"]', email); await page.fill('input[name="passwort"]', passwort); await page.click('button[type="submit"]'); await page.waitForURL(`**/geraete/${geraet.id}`); await expect(page.getByRole("heading", { name: "PC-001" })).toBeVisible(); }); test("registriert einen vorgedruckten Tag und arbeitet daran weiter", async ({ page }) => { test.slow(); const [code, vorlagenName] = await Promise.all([tagAnlegen(), vorlageAnlegen()]); await anmelden(page); // Klein geschrieben, so wie ein Handy es eintippt. await page.goto(`/t/${code.toLowerCase()}`); await expect(page.getByRole("heading", { name: "Gerät registrieren" })).toBeVisible(); const hostname = `ZZ-E2E-${STEMPEL}`; await page.getByLabel("Hostname").fill(hostname); await page.getByLabel("Typ").selectOption("NOTEBOOK"); await page.getByRole("button", { name: "Gerät registrieren" }).click(); // Danach steht der Techniker auf der Geräteseite – nicht auf einer // Erfolgsmeldung, die er wegklicken muss. await page.waitForURL(/\/geraete\/[^/]+$/); await expect(page.getByRole("heading", { name: hostname })).toBeVisible(); await expect(page.getByText(code)).toBeVisible(); // Derselbe Aufkleber führt von jetzt an direkt hierher. const adresse = page.url(); await page.goto(`/t/${code}`); await page.waitForURL(adresse); // Eine Vorlage zuweisen: der Lauf erscheint als „offen“ unter Zugewiesen. /* * Ueber die Ueberschrift und nicht ueber den Text: der Abschnitt „Vorlage * zuweisen“ erklaert sich mit dem Satz „… erscheint oben unter * Zugewiesen“ — ein Textfilter faende ihn damit unter beiden Namen. */ const zuweisen = page .locator("section") .filter({ has: page.getByRole("heading", { name: "Vorlage zuweisen" }) }); // Ueber den Wert, nicht ueber die Beschriftung: die traegt Kategorie und // Version und liesse sich mit jeder Feinheit am Text brechen. const wert = await zuweisen.locator("option", { hasText: vorlagenName }).getAttribute("value"); if (!wert) throw new Error(`Die Vorlage „${vorlagenName}“ steht nicht zur Auswahl.`); await zuweisen.getByLabel("Vorlage").selectOption(wert); await zuweisen.getByRole("button", { name: "Lauf zuweisen" }).click(); const zugewiesen = page .locator("section") .filter({ has: page.getByRole("heading", { name: "Zugewiesen" }) }); await expect(zugewiesen.getByText("offen")).toBeVisible(); // Und „Starten“ öffnet ihn wirklich – danach ist er in Arbeit. await zugewiesen.getByRole("button", { name: "Starten" }).click(); await page.waitForURL(/\/laeufe\/[^/]+$/); await expect(page.getByRole("button", { name: "Lauf abschließen" })).toBeVisible(); }); test("trägt am Handy ohne horizontales Scrollen", async ({ page, isMobile }) => { test.skip(!isMobile, "Nur im Projekt „handy“."); await anmelden(page); await page.goto("/geraete"); await page.getByRole("link", { name: "NB-001" }).first().click(); await page.waitForURL(/\/geraete\/[^/]+$/); const masse = await page.evaluate(() => ({ scroll: document.documentElement.scrollWidth, sichtbar: document.documentElement.clientWidth, })); expect(masse.scroll).toBeLessThanOrEqual(masse.sichtbar); }); }); test.afterAll(async () => { const vorlagen = await prisma.vorlage.findMany({ where: { name: { startsWith: "ZZ E2E Geraetelauf" } }, select: { id: true }, }); /* * Die Testgeraete und ihre Laeufe raeumen sich nicht von selbst weg: ein * Tag-Code bleibt absichtlich fuer immer vergeben, das Geraet dahinter muss * aber nicht stehen bleiben. */ /* * Nur die eigenen: ein „alle Tags ohne Zuordnung“ raeumte auch den Vorrat * weg, den jemand fuer echte Aufkleber angelegt hat — in einer * Entwicklungsdatenbank ist das derselbe Bestand. */ const geraete = await prisma.geraet.findMany({ where: { OR: [{ hostname: { startsWith: "ZZ-E2E-" } }, { tagCode: { in: eigeneTags } }] }, select: { id: true }, }); const kennungen = geraete.map((geraet) => geraet.id); if (kennungen.length > 0) { const laeufe = await prisma.lauf.findMany({ where: { geraetId: { in: kennungen } }, select: { id: true }, }); const laufIds = laeufe.map((lauf) => lauf.id); if (laufIds.length > 0) { await prisma.kommentar.deleteMany({ where: { laufId: { in: laufIds } } }); await prisma.schrittergebnis.deleteMany({ where: { laufId: { in: laufIds } } }); await prisma.protokoll.deleteMany({ where: { OR: [{ laufId: { in: laufIds } }, { entitaetId: { in: laufIds } }] }, }); await prisma.lauf.deleteMany({ where: { id: { in: laufIds } } }); } await prisma.protokoll.deleteMany({ where: { entitaetId: { in: kennungen } } }); await prisma.geraet.deleteMany({ where: { id: { in: kennungen } } }); } // Die Testvorlage zuletzt: an ihr hingen gerade noch die Laeufe. if (vorlagen.length > 0) { await prisma.vorlage.deleteMany({ where: { id: { in: vorlagen.map((zeile) => zeile.id) } } }); } await prisma.$disconnect(); });