ESLint hatte keine Konfiguration — das Skript "npm run lint" pruefte nichts. Jetzt Flat Config ohne Formatierungsregeln, dafuer mit den Regeln, die die Fehler dieses Projekts gemeldet haetten. Er hat gleich zwei gefunden, die niemand gesucht hatte. Das Badge rendert ein span statt eines div. Damit ist die Meldung "div cannot be a descendant of p" weg, die seit Phase 3 auf der Einsatzseite stand — sie ist der klassische Ausloeser fuer einen Hydration-Fehler. Die E2E-Tests laufen gegen eine eigene Wehr. Nach fuenf Phasen standen in der Beispielwehr 120 Atemschutz-Ueberwachungen statt einer und 115 Geraete statt 45; bei jedem Lauf aenderten sich alle Bildschirmfotos. Der Bestand ist einmal zurueckgesetzt, npm run db:test-reset baut die Testwehr neu auf. Behoben nebenbei: - Die Fehlerflaeche der Karte konnte nie etwas anzeigen. - Zwei useMemo der Lagekarte rechneten bei jedem Rendern neu. - tests/e2e war vom Typecheck ausgenommen und liess einen doppelt deklarierten Bezeichner durch. Closes #10 Closes #11 Closes #12 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
224 lines
7.9 KiB
TypeScript
224 lines
7.9 KiB
TypeScript
import { expect, test, type Locator, type Page } from "@playwright/test";
|
|
import { anmelden } from "./wehr";
|
|
|
|
/**
|
|
* E2E-Tests für das Geräteformular.
|
|
*
|
|
* Der interessante Teil ist nicht das Speichern, sondern die Lagerungsregel:
|
|
* Ein Gerät steht entweder an einem Standort oder auf einem Fahrzeug. Das
|
|
* Formular führt das als Umschalter, damit der Fehler gar nicht erst möglich
|
|
* ist — und die Fristenvorschau zeigt vorab, was der Server daraus rechnen wird.
|
|
*
|
|
* Zwei Eigenheiten, die die Selektoren hier erklären:
|
|
*
|
|
* - `getByLabel` vergleicht den Text des `<label>`, nicht den zugänglichen
|
|
* Namen. An Pflichtfeldern steht dort ein Sternchen, das für Hilfsmittel
|
|
* ausgeblendet ist. Deshalb wird über die Rolle gesucht: der Name, den eine
|
|
* Sprachausgabe vorliest, ist auch der, auf den sich der Test verlässt.
|
|
* - `fill()` greift bei `input[type=date]` in Chromium nicht durch. Datumsfelder
|
|
* werden deshalb getippt, so wie es ein Mensch auch täte.
|
|
*/
|
|
|
|
/** Eindeutig je Testlauf, damit sich parallele Läufe nicht ins Gehege kommen. */
|
|
function testnummer(kennzeichen: string): string {
|
|
return `E2E-${kennzeichen}-${Date.now().toString().slice(-6)}`;
|
|
}
|
|
|
|
/** Tippt ein Datum in ein date-Feld. Reihenfolge nach deutscher Locale. */
|
|
async function datumTippen(
|
|
feld: Locator,
|
|
tag: string,
|
|
monat: string,
|
|
jahr: string,
|
|
) {
|
|
await feld.click();
|
|
await feld.pressSequentially(`${tag}${monat}${jahr}`, { delay: 30 });
|
|
}
|
|
|
|
function feld(page: Page, name: string): Locator {
|
|
return page.getByRole("textbox", { name, exact: true });
|
|
}
|
|
|
|
function zahlfeld(page: Page, name: string): Locator {
|
|
return page.getByRole("spinbutton", { name, exact: true });
|
|
}
|
|
|
|
function auswahl(page: Page, name: string): Locator {
|
|
return page.getByRole("combobox", { name, exact: true });
|
|
}
|
|
|
|
/** Füllt die beiden Pflichtfelder und wählt eine Lagerung. */
|
|
async function grunddatenAusfuellen(
|
|
page: Page,
|
|
nummer: string,
|
|
bezeichnung: string,
|
|
) {
|
|
await feld(page, "Inventarnummer").fill(nummer);
|
|
await feld(page, "Bezeichnung").fill(bezeichnung);
|
|
}
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await anmelden(page);
|
|
});
|
|
|
|
test.describe("Gerät anlegen", () => {
|
|
test("legt ein Gerät an einem Standort an und öffnet es danach", async ({
|
|
page,
|
|
}) => {
|
|
const nummer = testnummer("STANDORT");
|
|
|
|
await page.goto("/geraete/neu");
|
|
await expect(
|
|
page.getByRole("heading", { name: "Neues Gerät" }),
|
|
).toBeVisible();
|
|
|
|
await grunddatenAusfuellen(page, nummer, "Testgerät Standort");
|
|
await feld(page, "Hersteller").fill("Prüffirma");
|
|
|
|
await auswahl(page, "Standort").click();
|
|
await page.getByRole("option").first().click();
|
|
|
|
await page.getByRole("button", { name: "Gerät anlegen" }).click();
|
|
|
|
await page.waitForURL(/\/geraete\/[0-9a-f-]{36}$/);
|
|
await expect(
|
|
page.getByRole("heading", { name: "Testgerät Standort" }),
|
|
).toBeVisible();
|
|
await expect(page.getByText(nummer).first()).toBeVisible();
|
|
});
|
|
|
|
test("legt ein Gerät auf einem Fahrzeug samt Geräteraum an", async ({
|
|
page,
|
|
}) => {
|
|
const nummer = testnummer("FAHRZEUG");
|
|
|
|
await page.goto("/geraete/neu");
|
|
await grunddatenAusfuellen(page, nummer, "Testgerät Fahrzeug");
|
|
|
|
await page.getByRole("radio", { name: "Auf einem Fahrzeug" }).click();
|
|
|
|
await auswahl(page, "Fahrzeug").click();
|
|
await page
|
|
.getByRole("option", { name: /Florian Musterhausen 40\/1/ })
|
|
.click();
|
|
|
|
await auswahl(page, "Geräteraum").click();
|
|
await page.getByRole("option", { name: /^G1/ }).click();
|
|
|
|
await page.getByRole("button", { name: "Gerät anlegen" }).click();
|
|
|
|
await page.waitForURL(/\/geraete\/[0-9a-f-]{36}$/);
|
|
await expect(page.getByText(/Verlastet auf/)).toBeVisible();
|
|
await expect(page.getByText(/G1/).first()).toBeVisible();
|
|
});
|
|
|
|
test("verlangt eine Angabe zur Lagerung, statt sie stillschweigend wegzulassen", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/geraete/neu");
|
|
await grunddatenAusfuellen(page, testnummer("OHNE"), "Testgerät ohne Ort");
|
|
|
|
// Auf Fahrzeug umschalten, aber keines wählen.
|
|
await page.getByRole("radio", { name: "Auf einem Fahrzeug" }).click();
|
|
await page.getByRole("button", { name: "Gerät anlegen" }).click();
|
|
|
|
await expect(page.getByTestId("formularfehler")).toContainText(
|
|
"Fahrzeug auswählen",
|
|
);
|
|
// Nichts gespeichert: die Adresse hat sich nicht geändert.
|
|
await expect(page).toHaveURL(/\/geraete\/neu$/);
|
|
});
|
|
|
|
test("rechnet die Fristen schon beim Ausfüllen vor", async ({
|
|
page,
|
|
}, angaben) => {
|
|
// Unter Touch-Emulation nimmt Chromium keine zeichenweise Eingabe in
|
|
// Datumsfelder an — dort öffnet sich der Auswähler des Betriebssystems.
|
|
// Die Rechnung selbst prüfen die Unit-Tests in qr-und-fristen.test.ts;
|
|
// hier geht es nur um die Verdrahtung, und die deckt der Desktop ab.
|
|
test.skip(
|
|
angaben.project.name === "tablet",
|
|
"Datumsfelder lassen sich unter Touch-Emulation nicht tippen.",
|
|
);
|
|
|
|
await page.goto("/geraete/neu");
|
|
|
|
await expect(page.getByText("Nächste Prüfung:")).toBeVisible();
|
|
await expect(page.getByText("keine", { exact: true })).toBeVisible();
|
|
|
|
await datumTippen(feld(page, "Indienststellung"), "01", "03", "2026");
|
|
await zahlfeld(page, "Prüfintervall (Monate)").fill("12");
|
|
|
|
await expect(page.getByText("01.03.2027")).toBeVisible();
|
|
|
|
await zahlfeld(page, "Lebensdauer (Jahre)").fill("10");
|
|
await expect(page.getByText("01.03.2036")).toBeVisible();
|
|
});
|
|
|
|
test("übernimmt Prüfintervall und Lebensdauer aus dem Gerätetyp", async ({
|
|
page,
|
|
}, angaben) => {
|
|
test.skip(
|
|
angaben.project.name === "tablet",
|
|
"Datumsfelder lassen sich unter Touch-Emulation nicht tippen.",
|
|
);
|
|
|
|
await page.goto("/geraete/neu");
|
|
|
|
await auswahl(page, "Gerätetyp").click();
|
|
await page.getByRole("option", { name: "Pressluftatmer PA 94" }).click();
|
|
|
|
// Die Bezeichnung wird aus dem Typ vorbelegt, solange sie leer ist.
|
|
await expect(feld(page, "Bezeichnung")).toHaveValue("Pressluftatmer PA 94");
|
|
|
|
await datumTippen(feld(page, "Indienststellung"), "01", "03", "2026");
|
|
|
|
await expect(page.getByText(/aus dem Gerätetyp/)).toBeVisible();
|
|
await expect(page.getByText("01.03.2027")).toBeVisible();
|
|
// Pressluftatmer haben 20 Jahre Lebensdauer ab Indienststellung.
|
|
await expect(page.getByText("01.03.2046")).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe("Gerät bearbeiten", () => {
|
|
test("ändert ein vorhandenes Gerät und protokolliert es", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/geraete");
|
|
await page.getByRole("link", { name: "INV-0002" }).click();
|
|
await page.getByRole("link", { name: "Bearbeiten" }).click();
|
|
|
|
await expect(feld(page, "Inventarnummer")).toHaveValue("INV-0002");
|
|
|
|
const bemerkung = `Vom E2E-Test geändert um ${Date.now()}`;
|
|
await feld(page, "Bemerkung").fill(bemerkung);
|
|
await page.getByRole("button", { name: "Änderungen speichern" }).click();
|
|
|
|
await page.waitForURL(/\/geraete\/[0-9a-f-]{36}$/);
|
|
await expect(page.getByText(bemerkung)).toBeVisible();
|
|
|
|
// Die Änderung steht im Verlauf. Bei wiederholten Läufen sammeln sich dort
|
|
// mehrere Einträge — der Log ist append-only, das ist so gewollt.
|
|
await expect(
|
|
page.getByText(/Gerät INV-0002 geändert/).first(),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("schaltet die Lagerung von Fahrzeug auf Standort um", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/geraete");
|
|
await page.getByRole("link", { name: "INV-0003" }).click();
|
|
await page.getByRole("link", { name: "Bearbeiten" }).click();
|
|
|
|
await page.getByRole("radio", { name: "An einem Standort" }).click();
|
|
await auswahl(page, "Standort").click();
|
|
await page.getByRole("option").first().click();
|
|
|
|
await page.getByRole("button", { name: "Änderungen speichern" }).click();
|
|
await page.waitForURL(/\/geraete\/[0-9a-f-]{36}$/);
|
|
|
|
await expect(page.getByText(/^Standort /)).toBeVisible();
|
|
});
|
|
});
|