feat(laeufe): Service, Abarbeitung, Kommentare und Archivschutz
Ein Lauf wird vorbereitet, abgearbeitet, abgeschlossen und archiviert. Der Archiv-Schreibschutz sitzt in genau einer Funktion und wird vor jeder Aenderung an Kopfdaten, Schrittergebnis und Kommentar gerufen; abgeschlossen bleibt aenderbar, archiviert nicht. Die Abarbeitungsansicht speichert jeden Schritt einzeln, frischt sich alle zehn Sekunden auf und pausiert im Hintergrund. Sie traegt in der Polling-Antwort keine Befehle und laedt sich auch nicht von selbst neu, solange ein generiertes Passwort auf der Seite steht: beides tauschte es dem Techniker zwischen Lesen und Einfuegen aus. Der unabhaengige Review fand zwei Blocker: ein Befehl konnte zwei verschiedene Passwoerter tragen (Konto bekommt A, Benutzer erfaehrt B), und „nicht zutreffend“ liess sich nicht zuruecknehmen, waehrend die Oberflaeche behauptete, nichts sei gewaehlt. Beides behoben und in Tests festgehalten. Closes #13 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
eec5de404a
commit
ed6e6b81eb
+42
-2
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { chromium, type Browser, type Cookie, type Page } from "@playwright/test";
|
||||
import { chromium, type Browser, type Cookie, type Locator, type Page } from "@playwright/test";
|
||||
|
||||
const APP_URL = process.env["E2E_APP_URL"] ?? "http://127.0.0.1:3000";
|
||||
|
||||
@@ -42,6 +42,12 @@ type Seite = {
|
||||
readonly nurModus?: "INTERN" | "DIENSTLEISTUNG";
|
||||
/** Vor der Aufnahme ausfuehren, etwa ein Aufklappen. */
|
||||
readonly vorbereiten?: (tab: Page) => Promise<void>;
|
||||
/**
|
||||
* Nur diesen Ausschnitt aufnehmen statt der ganzen Seite. Fuer Bilder, die
|
||||
* ein einzelnes Element zeigen sollen — eine ganze Seite waere dasselbe Bild
|
||||
* wie nebenan, nur groesser.
|
||||
*/
|
||||
readonly ausschnitt?: (tab: Page) => Locator;
|
||||
};
|
||||
|
||||
/** Seiten, die abgebildet werden. Waechst mit jedem Meilenstein. */
|
||||
@@ -124,6 +130,36 @@ const SEITEN: readonly Seite[] = [
|
||||
},
|
||||
},
|
||||
{ name: "geraete-liste", pfad: "/geraete" },
|
||||
{ name: "laeufe-liste", pfad: "/laeufe" },
|
||||
{ name: "lauf-starten", pfad: "/laeufe/starten" },
|
||||
{
|
||||
name: "lauf-abarbeiten",
|
||||
pfad: "/laeufe",
|
||||
vorbereiten: async (tab) => {
|
||||
await tab
|
||||
.getByRole("link", { name: /NB-001/ })
|
||||
.first()
|
||||
.click();
|
||||
await tab.waitForURL(/\/laeufe\/[^/]+$/);
|
||||
},
|
||||
},
|
||||
{
|
||||
// Der Befehlsschritt, aufgeklappt und in den Blick gescrollt: am Handy
|
||||
// stuende er sonst weit unterhalb des Bildrands.
|
||||
name: "lauf-befehlsschritt",
|
||||
pfad: "/laeufe",
|
||||
vorbereiten: async (tab) => {
|
||||
await tab
|
||||
.getByRole("link", { name: /NB-001/ })
|
||||
.first()
|
||||
.click();
|
||||
await tab.waitForURL(/\/laeufe\/[^/]+$/);
|
||||
const schritt = tab.getByRole("listitem").filter({ hasText: "Rechner umbenannt" });
|
||||
await schritt.scrollIntoViewIfNeeded();
|
||||
await tab.waitForTimeout(150);
|
||||
},
|
||||
ausschnitt: (tab) => tab.getByRole("listitem").filter({ hasText: "Rechner umbenannt" }),
|
||||
},
|
||||
{ name: "abteilungen", pfad: "/admin/abteilungen" },
|
||||
];
|
||||
|
||||
@@ -226,7 +262,11 @@ try {
|
||||
await tab.waitForLoadState("domcontentloaded");
|
||||
|
||||
const datei = join(ziel, `${seite.name}-${ansicht.name}.png`);
|
||||
await tab.screenshot({ path: datei, fullPage: ansicht.name === "desktop" });
|
||||
if (seite.ausschnitt) {
|
||||
await seite.ausschnitt(tab).screenshot({ path: datei });
|
||||
} else {
|
||||
await tab.screenshot({ path: datei, fullPage: ansicht.name === "desktop" });
|
||||
}
|
||||
|
||||
// Am Handy darf nie horizontal gescrollt werden – hier gleich mitgeprueft.
|
||||
if (ansicht.name === "handy") {
|
||||
|
||||
Reference in New Issue
Block a user