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). * * Die Suche selbst prueft `tests/archiv.test.ts` gegen die Datenbank, bis in die * 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(); /** 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. * * Direkt ueber Prisma und nicht ueber die Dienste: hier soll **das Archiv** * 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 { laufende += 1; const marke = `${STEMPEL}-${laufende}`; const suchwort = `Restposten ${marke}`; const version = await prisma.vorlagenVersion.findFirstOrThrow({ 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({ where: { istEigeneOrganisation: true }, select: { id: true }, }); const schrittId = version.abschnitte.flatMap((abschnitt) => abschnitt.schritte)[0]?.id; if (!schrittId) throw new Error(`Die Vorlage „${SEED_VORLAGE}“ hat keinen Schritt.`); const jetzt = new Date(); const titel = `ZZ E2E Archiv ${marke}`; const lauf = await prisma.lauf.create({ data: { vorlagenVersionId: version.id, kundeId: kunde.id, titel, status: "ARCHIVIERT", gestartetAm: jetzt, abgeschlossenAm: jetzt, archiviertAm: jetzt, }, }); await prisma.schrittergebnis.create({ data: { laufId: lauf.id, vorlagenSchrittId: schrittId, zustand: "NICHT_ZUTREFFEND", notiz: `${suchwort} aus dem Lager`, }, }); return { id: lauf.id, titel, suchwort }; } test.describe("Archiv", () => { test("findet einen Lauf über ein Wort aus einer Schritt-Notiz", async ({ page }) => { const lauf = await archivierterLauf(); await anmelden(page); // Am Handy steckt die Navigation im Menü – der Weg dorthin gehört zum Test. if (test.info().project.name === "handy") { await page.getByLabel("Menü").click(); } await page.getByRole("link", { name: "Archiv" }).first().click(); await page.waitForURL("**/archiv"); await page.getByLabel("Suchbegriff").fill(lauf.suchwort); await page.getByRole("button", { name: "Suchen" }).click(); const treffer = page.getByRole("listitem").filter({ hasText: lauf.titel }); await expect(treffer).toHaveCount(1); // Und der Grund steht dabei: Fundstelle und Ausschnitt. await expect(treffer.getByText("Notiz")).toBeVisible(); await expect(treffer.getByText(/Restposten/)).toBeVisible(); await treffer.getByRole("link").first().click(); await page.waitForURL(`**/laeufe/${lauf.id}`); await expect(page.getByText(/Dieser Lauf ist archiviert/)).toBeVisible(); }); test("sagt, dass ein einzelnes Zeichen nicht sucht", async ({ page }) => { // Ein stilles Nichtstun wäre die schlechtere Antwort: der Suchende wartet. await anmelden(page); await page.goto("/archiv?suche=a"); // Die Meldung **zum Ergebnis**, nicht der Hinweis am Formular: beide nennen // die zwei Zeichen, und nur die erste sagt, dass gerade nichts gesucht wurde. await expect(page.getByText(/ist als Suchbegriff zu kurz/)).toBeVisible(); }); 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=${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, })); expect(masse.scroll).toBeLessThanOrEqual(masse.sichtbar); }); }); 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 }, }); 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.$disconnect(); });