150 lines
5.9 KiB
TypeScript
150 lines
5.9 KiB
TypeScript
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();
|
||
});
|