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); }); });