feat(doku): Anleitung, PDF und Ende-zu-Ende-Abnahme (#36)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #36.
This commit is contained in:
2026-09-26 09:04:15 +00:00
parent 9bd198a531
commit fbbb634e56
104 changed files with 4527 additions and 299 deletions
+110 -1
View File
@@ -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) {