Files
Laufzettel/tests/e2e/archiv.spec.ts
T
Florian WildeandClaude Opus 5 8146e33f48
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(archiv): Uebersicht, Volltextsuche und Druckansicht
Der Einstieg am Schreibtisch und das Gedaechtnis der Anwendung. Die Uebersicht
sortiert nach Dringlichkeit — ueberfaellig zuerst, und das ist das Einzige in
Warnfarbe; alles andere auf der Seite ist Auskunft.

Das Archiv sucht ueber Titel, Notizen, erfasste Werte und Kommentare in einer
SQL-Anweisung: `Schrittergebnis.wert` ist ein Json, und nur `("wert")::text`
trifft den Trigramm-Index aus #006. Vor allem aber gehoert die Fundstelle zum
Treffer — ein Treffer ohne sichtbaren Grund ist fuer den Suchenden wertlos.

Die Druckansicht ist ein eigenes Lesemodell und kein Schalter an
`laufZumAbarbeiten`: dort entsteht beim Fuellen der Befehle ein Passwort, und auf
einem Blatt, das jemand abheftet, darf keines stehen. Vom Mitarbeiter holt sie
nur Name, UPN und Abteilung — nicht alles und dann gefiltert.

Der unabhaengige Review fand zwei Blocker, beide im Dashboard: „ueberfaellig“
verglich mit dem Augenblick statt mit dem Tagesbeginn, und der Schnellstart
zaehlte Laeufe fremder Kunden mit. Was „ueberfaellig“ heisst, steht seitdem in
`src/lib/fristen.ts` — eine Regel fuer Lesemodell und Anzeige.

Closes #15

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 22:59:02 +02:00

130 lines
5.0 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, type Page } from "@playwright/test";
import "dotenv/config";
import { prisma } from "@/lib/db";
/**
* 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.
*/
const STEMPEL = Date.now();
const NOTIZ = `Restposten ${STEMPEL} aus dem Lager`;
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 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.
*/
async function archivierterLauf(): Promise<{ id: string; titel: string }> {
const version = await prisma.vorlagenVersion.findFirstOrThrow({
where: { status: "VEROEFFENTLICHT" },
orderBy: { veroeffentlichtAm: "desc" },
select: { id: true, abschnitte: { select: { schritte: { select: { id: true } } } } },
});
const kunde = await prisma.kunde.findFirstOrThrow({ select: { id: true } });
const schrittId = version.abschnitte.flatMap((abschnitt) => abschnitt.schritte)[0]?.id;
if (!schrittId) throw new Error("Die Vorlage hat keinen Schritt.");
const jetzt = new Date();
const titel = `ZZ E2E Archiv ${STEMPEL}`;
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: NOTIZ,
},
});
return { id: lauf.id, titel };
}
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(`Restposten ${STEMPEL}`);
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“.");
await anmelden(page);
await page.goto("/archiv?suche=Restposten");
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({
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();
});