Files
Laufzettel/tests/e2e/geraeteseite.spec.ts
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

238 lines
10 KiB
TypeScript
Raw Permalink 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_TAG } from "./helfer";
/**
* 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/<code>`, das Registrieren, die Geraeteseite und der Lauf.
*
* Voraussetzungen prueft `tests/e2e/global-setup.ts`. Das Beispielgeraet wird
* ueber `SEED_TAG` gesucht und nicht ueber den Hostnamen „PC-001“: im Modus
* DIENSTLEISTUNG traegt der Name das Kundenkuerzel, und in einer benutzten
* Datenbank gibt es „PC-001“ mehr als einmal (#018).
*/
const STEMPEL = Date.now();
/** Praefix der Geraete, die dieser Ablauf anlegt – und nur dieser. */
const HOSTNAME_PRAEFIX = "ZZ-E2E-TAG-";
/**
* 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<string> {
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<string> {
const { tagsErzeugen } = await import("@/server/services/tags");
const admin = await prisma.benutzer.findFirstOrThrow({
where: { rolle: "ADMIN" },
select: { id: true, rolle: true },
});
/*
* Der Kunde muss mit: im Modus DIENSTLEISTUNG verlangt `kundeErmitteln` eine
* Wahl und wirft sonst „Im Modus Dienstleistung muss ein Kunde gewählt
* werden“. Genommen wird der Kunde des Beispielgeraets — an ihm haengt auch
* die Geraeteseite, die dieser Ablauf danach oeffnet. Im Modus INTERN wird die
* Kennung verworfen, die Angabe schadet dort also nicht (#008).
*/
const beispiel = await prisma.geraet.findUniqueOrThrow({
where: { tagCode: SEED_TAG },
select: { kundeId: true },
});
const stapel = await tagsErzeugen(1, beispiel.kundeId, { 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.findUniqueOrThrow({
where: { tagCode: SEED_TAG },
select: { id: true, hostname: true },
});
await page.context().clearCookies();
await page.goto(`/t/${SEED_TAG}`);
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}`);
// Der Name kommt aus der Datenbank: er heisst nicht in jedem Modus „PC-001“.
await expect(page.getByRole("heading", { name: geraet.hostname ?? SEED_TAG })).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 = `${HOSTNAME_PRAEFIX}${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“.");
/*
* Gesucht wird ueber den Tag-Code und nicht ueber „NB-001“: der Name war eine
* Annahme ueber den Beispielbestand, und in einer benutzten Datenbank fuehrt
* er auf mehr als ein Geraet. Der Code ist eindeutig — die Liste zeigt danach
* genau eine Zeile, und der Weg ueber die Liste bleibt der des Technikers.
*/
await anmelden(page);
await page.goto(`/geraete?suche=${SEED_TAG}`);
await page.locator("ul > li").filter({ hasText: SEED_TAG }).getByRole("link").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({
/*
* Der Praefix gehoert dieser Datei allein. Vorher stand hier `ZZ-E2E-`, und
* damit raeumte dieser Nachlauf auch die Geraete aus
* `tests/e2e/geraete.spec.ts` weg — fremde Daten, wenn auch Testdaten (#018).
*/
where: { OR: [{ hostname: { startsWith: HOSTNAME_PRAEFIX } }, { 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();
});