feat(stammdaten): Mitarbeiter, Abteilungen und Geraete (#25)
This commit was merged in pull request #25.
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import "dotenv/config";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Voraussetzung: ein **gebauter** Stand laeuft (`pnpm build && pnpm start`) und
|
||||
* `SEED_ADMIN_EMAIL`/`SEED_ADMIN_PASSWORD` sind gesetzt.
|
||||
*
|
||||
* 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()}`;
|
||||
|
||||
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"]')]);
|
||||
}
|
||||
|
||||
/** 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 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 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 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 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user