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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9bd198a531
commit
10e272654f
+110
-1
@@ -54,6 +54,20 @@ type Seite = {
|
||||
* wie nebenan, nur groesser.
|
||||
*/
|
||||
readonly ausschnitt?: (tab: Page) => Locator;
|
||||
/**
|
||||
* Bereiche, die im Bild hervorgehoben werden – Rahmen in der Signal-Lime mit
|
||||
* einer Nummer, auf die der Text der Anleitung sich beziehen kann (#018).
|
||||
* Ohne Angabe entsteht genau dasselbe Bild wie vorher.
|
||||
*/
|
||||
readonly markierungen?: readonly Markierung[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Eine Markierung im Bild. Die Nummer kommt aus der Reihenfolge – sie im Text
|
||||
* der Anleitung zu nennen ist der ganze Zweck („(1) zeigt den Fortschritt“).
|
||||
*/
|
||||
type Markierung = {
|
||||
readonly ziel: (tab: Page) => Locator;
|
||||
};
|
||||
|
||||
/** Seiten, die abgebildet werden. Waechst mit jedem Meilenstein. */
|
||||
@@ -137,7 +151,16 @@ const SEITEN: readonly Seite[] = [
|
||||
},
|
||||
{ name: "geraete-liste", pfad: "/geraete" },
|
||||
{ name: "laeufe-liste", pfad: "/laeufe" },
|
||||
{ name: "lauf-starten", pfad: "/laeufe/starten" },
|
||||
{
|
||||
name: "lauf-starten",
|
||||
pfad: "/laeufe/starten",
|
||||
// (1) Vorlage, (2) Geraet – die zwei Felder, ueber die das Kapitel „Lauf
|
||||
// starten“ redet; alles darunter ist freiwillig.
|
||||
markierungen: [
|
||||
{ ziel: (tab) => tab.locator('select[name="vorlagenVersionId"]') },
|
||||
{ ziel: (tab) => tab.locator('select[name="geraetId"]') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "lauf-abarbeiten",
|
||||
pfad: "/laeufe",
|
||||
@@ -148,6 +171,24 @@ const SEITEN: readonly Seite[] = [
|
||||
.click();
|
||||
await tab.waitForURL(/\/laeufe\/[^/]+$/);
|
||||
},
|
||||
// (1) Fortschritt, (2) Zustandspillen des ersten Schritts, (3) Kommentare.
|
||||
markierungen: [
|
||||
{ ziel: (tab) => tab.getByRole("progressbar") },
|
||||
{
|
||||
ziel: (tab) =>
|
||||
tab
|
||||
.getByRole("listitem")
|
||||
.filter({ has: tab.getByRole("button", { name: "erledigt", exact: true }) })
|
||||
.first()
|
||||
.getByRole("button", { name: "erledigt", exact: true })
|
||||
// Nicht die Pille allein, sondern die Zeile mit allen drei Zustaenden.
|
||||
.locator(".."),
|
||||
},
|
||||
{
|
||||
ziel: (tab) =>
|
||||
tab.locator("section").filter({ has: tab.getByRole("heading", { name: "Kommentare zum Lauf" }) }),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// Der Befehlsschritt, aufgeklappt und in den Blick gescrollt: am Handy
|
||||
@@ -176,6 +217,15 @@ const SEITEN: readonly Seite[] = [
|
||||
await tab.getByRole("link", { name: "NB-001" }).first().click();
|
||||
await tab.waitForURL(/\/geraete\/[^/]+$/);
|
||||
},
|
||||
// (1) der Weg zum QR-Code, (2) was schon zugewiesen ist,
|
||||
// (3) der Weg zu einer weiteren Vorlage.
|
||||
markierungen: [
|
||||
// Der Code selbst steht in einem zugeklappten `details` und waere im Bild
|
||||
// nicht zu sehen – markiert wird deshalb der Griff, der ihn aufklappt.
|
||||
{ ziel: (tab) => tab.locator("summary", { hasText: "QR-Code anzeigen" }) },
|
||||
{ ziel: (tab) => tab.getByRole("heading", { name: "Zugewiesen" }).locator("..") },
|
||||
{ ziel: (tab) => tab.getByRole("heading", { name: "Vorlage zuweisen" }).locator("..") },
|
||||
],
|
||||
},
|
||||
{
|
||||
/*
|
||||
@@ -340,6 +390,64 @@ async function kachelnErzeugen(browser: Browser): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt die Markierungen als Overlay in die Seite, bevor das Bild entsteht.
|
||||
*
|
||||
* Gezeichnet wird im Browser und in Dokumentkoordinaten (`getBoundingClientRect`
|
||||
* plus `scrollX`/`scrollY`), damit der Rahmen auch auf einem `fullPage`-Bild dort
|
||||
* sitzt, wo er hingehoert. Nachbearbeitet wird kein Bild – wer die Oberflaeche
|
||||
* aendert, laesst dieses Skript erneut laufen, und die Rahmen wandern mit.
|
||||
*
|
||||
* Findet eine Markierung ihr Ziel nicht, bricht der Lauf ab: ein Bild mit
|
||||
* fehlender „(2)“ macht den Text der Anleitung falsch.
|
||||
*/
|
||||
async function markierungenEinspritzen(
|
||||
tab: Page,
|
||||
seitenname: string,
|
||||
markierungen: readonly Markierung[],
|
||||
): Promise<void> {
|
||||
for (const [stelle, markierung] of markierungen.entries()) {
|
||||
const ziel = markierung.ziel(tab).first();
|
||||
if ((await ziel.count()) === 0) {
|
||||
throw new Error(`Markierung ${stelle + 1} von "${seitenname}" findet ihr Ziel nicht.`);
|
||||
}
|
||||
await ziel.evaluate((element, nummer) => {
|
||||
const kasten = element.getBoundingClientRect();
|
||||
const links = kasten.left + window.scrollX;
|
||||
const oben = kasten.top + window.scrollY;
|
||||
const rahmen = document.createElement("div");
|
||||
Object.assign(rahmen.style, {
|
||||
position: "absolute",
|
||||
left: `${links - 5}px`,
|
||||
top: `${oben - 5}px`,
|
||||
width: `${kasten.width + 10}px`,
|
||||
height: `${kasten.height + 10}px`,
|
||||
border: "2px solid #C6F135",
|
||||
borderRadius: "14px",
|
||||
pointerEvents: "none",
|
||||
zIndex: "2147483647",
|
||||
});
|
||||
const marke = document.createElement("div");
|
||||
marke.textContent = String(nummer);
|
||||
Object.assign(marke.style, {
|
||||
position: "absolute",
|
||||
// Am linken Rand rutscht die Nummer nach innen, sonst schnitte sie das Bild ab.
|
||||
left: links < 24 ? "4px" : "-14px",
|
||||
top: "-14px",
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "999px",
|
||||
background: "#C6F135",
|
||||
color: "#0B0D10",
|
||||
font: "700 15px/26px ui-sans-serif, system-ui, sans-serif",
|
||||
textAlign: "center",
|
||||
});
|
||||
rahmen.append(marke);
|
||||
document.body.append(rahmen);
|
||||
}, stelle + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const zielMeilenstein = argument("ziel");
|
||||
const nur = argument("nur");
|
||||
/** Welcher Betriebsmodus gerade eingestellt ist – steuert, welche Seiten passen. */
|
||||
@@ -376,6 +484,7 @@ try {
|
||||
// Schriften muessen geladen sein, sonst wackelt das Bild zwischen Laeufen.
|
||||
await tab.evaluate(() => document.fonts.ready);
|
||||
await tab.waitForLoadState("domcontentloaded");
|
||||
if (seite.markierungen) await markierungenEinspritzen(tab, seite.name, seite.markierungen);
|
||||
|
||||
const datei = join(ziel, `${seite.name}-${ansicht.name}.png`);
|
||||
if (seite.ausschnitt) {
|
||||
|
||||
Reference in New Issue
Block a user