Files
Laufzettel/tests/e2e/geraete.spec.ts
T
Florian WildeandClaude Opus 5 10e272654f
Prüfung / pruefung (push) Canceled after 0s
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
feat(doku): Anleitung, PDF und Ende-zu-Ende-Abnahme
Neun Kapitel unter docs/anleitung/ fuer Technik und Verwaltung, mit 35
Bildern aus `pnpm docs:screenshots` und einem Fehlerbehebungskapitel aus
echten Meldungen. `pnpm docs:pdf` macht daraus 72 Seiten A4 — hell
gedruckt, mit Deckblatt, Inhaltsverzeichnis und Seitenzahlen, ohne neue
Abhaengigkeit.

Die Ablaeufe pruefen ihre Voraussetzungen, bevor der erste startet:
antwortet ein gebauter Stand, steht der Seed-Bestand, und liest der
Server dieselbe Datenbank wie der Testprozess. Jeder Ablauf legt an, was
er braucht, und raeumt es weg. Vier neue kommen dazu: Hauptablauf,
Onboarding mit Vergleich der Zwischenablage, Rollen und 390 px auf
achtzehn Seiten.

Drei Fehler kamen dabei heraus. Das Projekt „handy" pruefte 412 px statt
der verlangten 390. Ein angemeldeter Benutzer ohne Recht erfuhr nie,
warum er abgewiesen wurde — die Meldung stand auf einer Seite, die ihn
sofort weiterleitet; jetzt gibt es /kein-zugriff. Und der
Verzeichnis-Ablauf schrieb die Attributzuordnung der ersten vorhandenen
Quelle um, in einer echten Installation also der produktiven.

101 Ablaeufe in INTERN und 97 in DIENSTLEISTUNG, beide ohne Fehlschlag.

Closes #18

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

196 lines
8.3 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 { anmelden, kundeWaehlen } from "./helfer";
/**
* Die Geraeteliste durch die Oberflaeche (#011).
*
* Diese Datei gibt es aus demselben Grund wie tests/e2e/vorlagen-editor.spec.ts:
* ein Screenshot sagt in dieser Umgebung **nichts** ueber Bedienbarkeit, weil
* `next dev` nicht hydriert (siehe CLAUDE.md, „Oberflaeche pruefen“). Geprueft
* wird deshalb, was nur mit angehaengtem React gelingt — anlegen, suchen,
* einlesen, loeschen — und anschliessend, ob es wirklich in der Datenbank steht:
* durch Neuladen.
*
* Voraussetzungen — ein gebauter Stand antwortet, der Seed-Bestand steht und der
* Server liest dieselbe Datenbank wie dieser Lauf — prueft
* `tests/e2e/global-setup.ts` vor dem ersten Ablauf (#018). Eigene Daten legt
* dieser Ablauf selbst an; auf vorhandene baut er nicht.
*
* Der Ablauf raeumt hinter sich auf: an den angelegten Geraeten haengt kein Lauf,
* sie lassen sich also loeschen. Ihre Tag-Codes bleiben vergeben, und genau das
* ist die Zusage aus #011 — ein Aufkleber zeigt nie auf ein zweites Geraet.
*/
const MARKE = `ZZ-E2E-${Date.now()}`;
/** Der Abschnitt „Gerät anlegen“ – eindeutig ueber seinen eigenen Knopf. */
function anlegenFormular(seite: Page) {
return seite.locator("section", { has: seite.getByRole("button", { name: "Gerät anlegen" }) });
}
function karte(seite: Page, name: string) {
return seite.locator("li").filter({ hasText: name });
}
/** Loescht alle Geraete, deren Name mit der Marke dieses Durchlaufs beginnt. */
async function aufraeumen(seite: Page) {
await seite.goto(`/geraete?suche=${MARKE}`);
for (let versuch = 0; versuch < 10; versuch += 1) {
const karten = seite.locator("li").filter({ hasText: MARKE });
if ((await karten.count()) === 0) return;
const erste = karten.first();
await erste.getByRole("button", { name: "löschen", exact: true }).click();
await erste.getByRole("button", { name: "ja, löschen" }).click();
await expect(karten).toHaveCount(await karten.count().then((anzahl) => anzahl - 1));
}
}
test.describe("Geräte", () => {
test("legt ein Gerät an, findet es in der Liste und sucht es", async ({ page }) => {
test.slow();
const name = `${MARKE}-PC-01`;
await anmelden(page);
await page.goto("/geraete");
// Der Reiter fuehrt zwischen Personen und Geraeten hin und her.
await expect(page.getByRole("link", { name: "Geräte", exact: true })).toHaveAttribute(
"aria-current",
"page",
);
const formular = anlegenFormular(page);
await formular.getByLabel("Hostname").fill(name);
await formular.getByLabel("Seriennummer").fill(`${MARKE}-SN`);
await formular.getByLabel("Typ").selectOption("NOTEBOOK");
await kundeWaehlen(formular);
await formular.getByRole("button", { name: "Gerät anlegen" }).click();
// Der Tag-Code steht in der Meldung – er kommt gleich auf den Aufkleber.
const meldung = formular.getByRole("status");
await expect(meldung).toContainText(/wurde angelegt, Tag-Code LZ-[A-Z2-9]{6}\./);
const tagCode = (await meldung.textContent())?.match(/LZ-[A-Z2-9]{6}/)?.[0];
expect(tagCode).toBeTruthy();
// Der Beweis, dass es nicht nur im Browser steht: neu laden.
await page.goto("/geraete");
await expect(karte(page, name)).toContainText("Notebook");
await expect(karte(page, name)).toContainText(tagCode!);
// Suche über den Hostname …
await page.locator('input[name="suche"]').fill(name);
await page.getByRole("button", { name: "Filtern" }).click();
await expect(page.locator("ul > li").filter({ hasText: MARKE })).toHaveCount(1);
// … und über den abgetippten Tag-Code ohne Präfix.
await page.locator('input[name="suche"]').fill(tagCode!.slice(3));
await page.getByRole("button", { name: "Filtern" }).click();
await expect(karte(page, name)).toBeVisible();
// Ein Suchbegriff, der zu nichts passt, sagt das auch.
await page.locator('input[name="suche"]').fill(`${MARKE}-gibt-es-nicht`);
await page.getByRole("button", { name: "Filtern" }).click();
await expect(page.getByText("Kein Gerät passt zu diesen Filtern.")).toBeVisible();
await aufraeumen(page);
});
test("ändert Stammdaten und archiviert statt zu löschen", async ({ page }) => {
test.slow();
const name = `${MARKE}-PC-02`;
await anmelden(page);
await page.goto("/geraete");
const formular = anlegenFormular(page);
await formular.getByLabel("Hostname").fill(name);
await kundeWaehlen(formular);
await formular.getByRole("button", { name: "Gerät anlegen" }).click();
await expect(formular.getByRole("status")).toContainText("wurde angelegt");
await page.goto(`/geraete?suche=${MARKE}`);
const meine = karte(page, name);
await meine.getByText("Stammdaten bearbeiten").click();
await meine.getByLabel("Seriennummer").fill(`${MARKE}-SN-NEU`);
await meine.getByRole("button", { name: "Speichern" }).click();
await expect(meine.getByRole("status")).toContainText("Gespeichert.");
// Und wieder der Beweis, dass es in der Datenbank steht.
await page.reload();
await expect(karte(page, name)).toContainText(`S/N ${MARKE}-SN-NEU`);
// Archivieren nimmt das Gerät aus der Liste, ohne es zu entfernen.
await karte(page, name).getByRole("button", { name: "archivieren" }).click();
await expect(karte(page, name)).toHaveCount(0);
await page.goto(`/geraete?suche=${MARKE}&archiviert=1`);
await expect(karte(page, name)).toContainText("archiviert");
await karte(page, name).getByRole("button", { name: "wieder freigeben" }).click();
await expect(karte(page, name).getByRole("button", { name: "archivieren" })).toBeVisible();
await aufraeumen(page);
});
test("liest eine CSV-Datei ein und meldet die kaputte Zeile einzeln", async ({ page }) => {
test.slow();
await anmelden(page);
await page.goto("/geraete");
const bereich = page.locator("details", { hasText: "Geräte aus einer CSV-Datei einlesen" });
await bereich.getByText("Geräte aus einer CSV-Datei einlesen").click();
await bereich
.locator('textarea[name="inhalt"]')
.fill(
[
"hostname;label;serialNumber;inventoryNumber;type;model",
`${MARKE}-CSV-GUT;;SN-GUT;INV-1;PC;OptiPlex`,
`${MARKE}-CSV-BOESE;;SN-BOESE;INV-2;Tablet;`,
].join("\r\n"),
);
await kundeWaehlen(bereich);
await bereich.getByRole("button", { name: "Einlesen" }).click();
await expect(bereich.getByRole("status")).toContainText("1 von 2 Zeilen angelegt, 1 Zeile abgewiesen.");
// Mit Zeilennummer, damit die Datei an genau dieser Stelle zu ändern ist.
await expect(bereich.getByText("Zeile 3")).toBeVisible();
await expect(bereich.getByText(/kein bekannter Gerätetyp/)).toBeVisible();
// Die gute Zeile steht in der Datenbank, die kaputte nicht.
await page.goto(`/geraete?suche=${MARKE}-CSV`);
await expect(karte(page, `${MARKE}-CSV-GUT`)).toBeVisible();
await expect(page.locator("ul > li").filter({ hasText: MARKE })).toHaveCount(1);
await aufraeumen(page);
});
test("trägt am Handy ohne horizontales Scrollen", async ({ page, isMobile, viewport }) => {
test.skip(!isMobile, "Nur im Projekt „handy“.");
test.slow();
const name = `${MARKE}-RAUM`;
await anmelden(page);
await page.goto("/geraete");
const formular = anlegenFormular(page);
await formular.getByLabel("Bezeichnung").fill(name);
await formular.getByLabel("Typ").selectOption("RAUM");
await kundeWaehlen(formular);
await formular.getByRole("button", { name: "Gerät anlegen" }).click();
await expect(formular.getByRole("status")).toContainText("wurde angelegt");
await page.goto(`/geraete?suche=${MARKE}`);
await expect(karte(page, name)).toBeVisible();
const masse = await page.evaluate(() => ({
scroll: document.documentElement.scrollWidth,
sichtbar: document.documentElement.clientWidth,
}));
expect(masse.scroll).toBeLessThanOrEqual(masse.sichtbar);
expect(viewport?.width).toBeLessThanOrEqual(430);
// Die Bedienelemente sind am Handy mindestens 44 px hoch.
const knopf = karte(page, name).getByRole("button", { name: "archivieren" });
const kasten = await knopf.boundingBox();
expect(kasten?.height ?? 0).toBeGreaterThanOrEqual(44);
await aufraeumen(page);
});
});