Files
Laufzettel/tests/e2e/archiv.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

150 lines
5.9 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, 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<Testlauf> {
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();
});