Files
dogfather-universe/pruef-verwaltung.mjs
T
DogFatherGitandClaude Opus 5 98fc04c666 Verwaltung: Projektstand aendern, Wuensche beziffern, im Projekt antworten
Drei Server-Funktionen existierten seit Tagen und hatten KEINE Oberflaeche.
Erst im Vergleich "was kann der Server" gegen "was ruft die Seite auf"
ist es aufgefallen:

  POST /admin/projekte/:id           projektAendern
  POST /admin/aenderungen/:id/beziffern
  POST /admin/projekt-nachricht

Jede davon schliesst einen Kreis, der bisher offen war.

1. DIE SCHMERZHAFTESTE LUECKE: DER STAND

Der Kunde sieht in seinem Portal GANZ OBEN die Phasenleiste und den
"naechsten Schritt". Beides liess sich nirgends aendern. Ein Projekt blieb
also fuer immer im Briefing stehen, egal wie weit es wirklich war -- und
der prominenteste Text im ganzen Kundenportal war dauerhaft leer.

Jetzt: sieben Phasen als Knoepfe (plus Pausiert/Abgebrochen daneben, denn
das sind keine Phasen, sondern Zustaende), "wer ist am Zug", naechster
Schritt, Richttermin, Zahlungsstand, Portfolio-Freigabe.

Phase und "wer ist am Zug" speichern SOFORT ohne Speichern-Knopf: Es ist
ein Klick auf genau einen Wert, ein zweiter Klick waere Zeremonie. Die
Textfelder haben einen Knopf, weil man beim Tippen zwischendurch nicht
speichern will.

2. AENDERUNGSWUENSCHE

Der Kunde konnte Ideen einreichen, seit es das Portal gibt. Beziffern ging
serverseitig auch -- nur gab es keine Oberflaeche. Der Kreis war offen: Er
schickt etwas los und hoert nie wieder davon.

Jetzt stehen alle Wuensche im Projekt, offene mit Feldern fuer Preis,
Dauer und einer kurzen Erklaerung. Ohne Preis geht nichts raus (geprueft)
-- ohne Preis kann der Kunde nicht entscheiden, und ein "Angebot" ohne
Zahl ist keins.

3. NACHRICHTEN ZUM PROJEKT

Getrennt vom allgemeinen Postfach, weil sie zum Projekt gehoeren und im
Portal auch dort erscheinen. Sie hier nicht zu haben hiess: Der Kunde
schreibt im Projekt, und ich kann ihm nur woanders antworten.

AUFBAU

Neuer Endpunkt /admin/projekte/:id/alles liefert Projekt, Aufgaben,
Fortschritt, Aenderungswuensche und Nachrichten in EINER Antwort. Fuenf
Anfragen hintereinander wuerden die Ansicht sichtbar ruckelnd aufbauen.

Dieselbe Aufteilung wie in der Anfrageansicht -- links die Arbeit
(Aufgaben), rechts das Steuern. Wer zwischen beiden Ansichten wechselt,
muss nicht umdenken.

GEPRUEFT: 69 Pruefungen, Computer und Handy, alle bestanden.

Die neuen Pruefungen schauen nicht darauf, ob sich ein Knopf einfaerbt --
das beweist nichts. Sie schneiden mit, was die Seite TATSAECHLICH an den
Server schickt: {"status":"entwicklung"}, {"restBezahlt":true},
{"preisEuro":250,"dauer":"3 Tage"}. Und sie pruefen den Fall, in dem
nichts rausgehen darf: Ohne Preis bleibt die Mitschrift leer.

Ein Fehler im Test selbst behoben: Die Nachrichtenzaehlung war
document-weit und zaehlte die Nachrichten der geschlossenen Projektansicht
mit -- geschlossen heisst nicht aus dem Dokument entfernt. Jetzt auf den
Postfach-Verlauf eingegrenzt.

Versionsstempel und Cache-Name auf v6.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-23 12:08:57 +02:00

372 lines
19 KiB
JavaScript

/* Prüft die neuen Bereiche der Verwaltung: Projekte mit Aufgabenliste
und das Postfach. Wieder mit abgefangenen Netzanfragen, aber mit
Daten, die den heiklen Faellen entsprechen:
- ein Punkt, der auf den KUNDEN wartet (muss auffallen)
- ein Punkt "nicht enthalten" (muss als solcher erkennbar sein)
- eine INTERNE Notiz im Postfach (darf nicht wie eine gesendete
Nachricht aussehen)
*/
import { chromium } from "playwright";
import { createServer } from "node:http";
import { readFileSync, existsSync } from "node:fs";
import { join, extname } from "node:path";
const W = process.cwd();
const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml",
".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json" };
const srv = createServer((q, r) => {
const p = join(W, decodeURIComponent(q.url.split("?")[0]));
if (existsSync(p) && !p.endsWith("/")) {
r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" });
return r.end(readFileSync(p));
}
r.writeHead(404).end("");
});
await new Promise((r) => srv.listen(4325, r));
const PROJEKT = {
id: "p1", nummer: "P-0001", titel: "Website für Musterbau", paket: "website",
status: "design", kunde_id: "k1", kunde_name: "Anna Muster", kunde_firma: "Musterbau",
preis_cent: 180000, waehrung: "EUR", richttermin: null, archiviert: 0,
anzahlung_bezahlt: 1, rest_bezahlt: 0, erstellt_am: new Date().toISOString(),
aufg_gesamt: 6, aufg_erledigt: 2, aufg_kunde: 2, ungelesen: 1,
naechster_schritt: "Deine Rueckmeldung zum Entwurf.", wartet_auf: "kunde",
kunde_email: "[email protected]", portfolio_freigabe: 0,
};
const AENDERUNGEN = [
{ id: "ae1", titel: "Noch eine Unterseite", beschreibung: "Eine Seite fuer Referenzen waere schoen.",
status: "eingereicht", zusatzpreis_cent: null, zusatzdauer_text: null,
erstellt_am: new Date(Date.now() - 86400000).toISOString() },
{ id: "ae2", titel: "Anderes Logo", beschreibung: "Wir haben ein neues.",
status: "angenommen", zusatzpreis_cent: 12000, zusatzdauer_text: "2 Tage",
erstellt_am: new Date(Date.now() - 172800000).toISOString() },
];
const PNACHRICHTEN = [
{ id: "pn1", autor: "kunde", text: "Wann kommt der naechste Entwurf?",
erstellt_am: new Date(Date.now() - 7200000).toISOString() },
{ id: "pn2", autor: "dogfather", text: "Morgen Abend.",
erstellt_am: new Date(Date.now() - 3600000).toISOString() },
];
const AUFGABEN = [
{ id: "a1", titel: "Briefing-Gespräch führen", beschreibung: "Ziel und Umfang klären.",
kategorie: "start", status: "erledigt", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null, reihenfolge: 10 },
{ id: "a2", titel: "Texte und Bilder zusammenstellen", beschreibung: "Lieber früh als vollständig.",
kategorie: "start", status: "offen", wer_dran: "kunde", nicht_enthalten: 0, hinweis: null, reihenfolge: 20 },
{ id: "a3", titel: "Entwurf der Startseite", beschreibung: null,
kategorie: "gestaltung", status: "erledigt", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null, reihenfolge: 110 },
{ id: "a4", titel: "Entwurf ansehen und Rückmeldung geben", beschreibung: null,
kategorie: "gestaltung", status: "offen", wer_dran: "kunde", nicht_enthalten: 0, hinweis: null, reihenfolge: 130 },
{ id: "a5", titel: "Alle Unterseiten bauen", beschreibung: null,
kategorie: "umsetzung", status: "in_arbeit", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null, reihenfolge: 210 },
{ id: "a6", titel: "Mehrsprachigkeit", beschreibung: null,
kategorie: "umsetzung", status: "offen", wer_dran: "dogfather", nicht_enthalten: 1,
hinweis: "Nicht im Paket enthalten — auf Wunsch als Zusatz.", reihenfolge: 260 },
{ id: "a7", titel: "Seite live schalten", beschreibung: null,
kategorie: "abschluss", status: "offen", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null, reihenfolge: 860 },
];
const FORTSCHRITT = {
gesamt: 6, erledigt: 2, inArbeit: 1, beimKunden: 2, prozent: 33,
jeKategorie: {
start: { gesamt: 2, erledigt: 1, inArbeit: 0 },
gestaltung: { gesamt: 2, erledigt: 1, inArbeit: 0 },
umsetzung: { gesamt: 1, erledigt: 0, inArbeit: 1 },
abschluss: { gesamt: 1, erledigt: 0, inArbeit: 0 },
},
};
const VERLAUF = [
{ id: "n1", autor: "kunde", text: "Hallo! Können wir das Logo noch etwas grösser machen?",
intern: 0, projekt_titel: null, erstellt_am: new Date(Date.now() - 7200000).toISOString() },
{ id: "n2", autor: "dogfather", text: "Klar, mache ich heute noch.",
intern: 0, projekt_titel: "Website für Musterbau", erstellt_am: new Date(Date.now() - 3600000).toISOString() },
{ id: "n3", autor: "dogfather", text: "Nicht vergessen: Rechnung erst nach Abnahme.",
intern: 1, projekt_titel: null, erstellt_am: new Date(Date.now() - 1800000).toISOString() },
];
const ergebnisse = [];
function pruefe(name, ok, zusatz) {
ergebnisse.push({ name, ok });
console.log((ok ? " ok " : " FEHL ") + name + (zusatz ? " -> " + zusatz : ""));
}
const browser = await chromium.launch();
for (const [wo, breite, hoehe] of [["Computer", 1440, 950], ["Handy", 390, 844]]) {
const seite = await browser.newPage({ viewport: { width: breite, height: hoehe } });
const fehlerImProtokoll = [];
seite.on("pageerror", (e) => fehlerImProtokoll.push(e.message));
/* Mitschrift dessen, was die Seite tatsächlich an den Server schickt.
Nur so lässt sich prüfen, ob ein Klick auch WIRKLICH das Richtige
speichert — dass sich ein Knopf einfärbt, beweist gar nichts. */
const gesendet = [];
let beziffert = null;
let projektNachricht = null;
// NUR die API abfangen. Ein Muster, das den ganzen Ordner /webdesign
// umfasst, trifft auch die Seite SELBST -- dann bekommt der Browser
// statt der HTML-Datei ein JSON serviert. Genau das ist beim ersten
// Lauf passiert: Die Seite blieb leer, und der Test meldete
// "Abzeichen fehlt" statt "Seite gar nicht geladen".
// (Und der Hinweis steht in Zeilenkommentaren, weil das Glob-Muster
// ein Sternchen-Schrägstrich enthält und einen Blockkommentar
// vorzeitig beenden würde -- auch das ist hier schon passiert.)
await seite.route("**/webdesign/admin/**", (route) => {
const u = route.request().url();
if (u.includes("/admin/projekte/p1/alles")) {
return route.fulfill({ json: { ok: true, projekt: PROJEKT, aufgaben: AUFGABEN,
fortschritt: FORTSCHRITT, aenderungen: AENDERUNGEN, nachrichten: PNACHRICHTEN } });
}
if (u.includes("/admin/projekte/p1/aufgaben")) {
return route.fulfill({ json: { ok: true, projekt: PROJEKT, aufgaben: AUFGABEN, fortschritt: FORTSCHRITT } });
}
if (u.match(/\/admin\/projekte\/p1$/)) {
gesendet.push(route.request().postDataJSON());
return route.fulfill({ json: { ok: true } });
}
if (u.includes("/admin/aenderungen/")) {
beziffert = route.request().postDataJSON();
return route.fulfill({ json: { ok: true, preisText: "250,00 €" } });
}
if (u.includes("/admin/projekt-nachricht")) {
projektNachricht = route.request().postDataJSON();
return route.fulfill({ json: { ok: true } });
}
if (u.includes("/admin/projekte")) return route.fulfill({ json: { ok: true, projekte: [PROJEKT] } });
if (u.includes("/admin/postfach-zaehler")) return route.fulfill({ json: { ok: true, ungelesen: 1 } });
if (u.includes("/admin/postfach/k1")) {
return route.fulfill({ json: { ok: true,
kunde: { id: "k1", name: "Anna Muster", email: "[email protected]", sprache: "de" },
nachrichten: VERLAUF, projekte: [{ id: "p1", titel: "Website für Musterbau" }] } });
}
if (u.includes("/admin/postfach")) {
return route.fulfill({ json: { ok: true, ungelesen: 1, verlaeufe: [{
kunde_id: "k1", name: "Anna Muster", firma: "Musterbau", email: "[email protected]",
anzahl: 3, ungelesen: 1, letzte: new Date(Date.now() - 1800000).toISOString(),
letzter_text: "Nicht vergessen: Rechnung erst nach Abnahme.", letzter_autor: "dogfather" }] } });
}
if (u.includes("/admin/anfragen")) return route.fulfill({ json: { ok: true, anfragen: [] } });
if (u.includes("/admin/kunden")) return route.fulfill({ json: { ok: true, kunden: [] } });
return route.fulfill({ json: { ok: true } });
});
await seite.route("**/auth/**", (r) => r.fulfill({ json: { ok: true, user: { name: "Test" } } }));
await seite.addInitScript(() => {
sessionStorage.setItem("wd-vw-token", "t");
sessionStorage.setItem("wd-sitzung-aktiv", "1");
});
await seite.goto("http://127.0.0.1:4325/webdesign/verwaltung.html");
await seite.waitForTimeout(900);
console.log("\n=== " + wo + " ===");
// Abzeichen am Reiter
const abz = await seite.locator("#vw-pf-zaehler").innerText().catch(() => "");
const abzSichtbar = await seite.locator("#vw-pf-zaehler").isVisible().catch(() => false);
pruefe(wo + ": Postfach-Abzeichen zeigt die ungelesene Nachricht", abzSichtbar && abz === "1", abz);
// ---- Projekte ----
await seite.click('[data-tab="projekte"]');
await seite.waitForTimeout(500);
pruefe(wo + ": Projektliste zeigt das Projekt", await seite.locator("[data-projekt]").count() === 1);
pruefe(wo + ": „wartet auf Kunde“ ist sichtbar", await seite.locator(".vw-wartet").count() === 1);
await seite.click("[data-projekt]");
await seite.waitForTimeout(600);
const m = await seite.evaluate(() => {
const gruppen = Array.from(document.querySelectorAll(".vw-aufg-gruppe")).map((g) => g.dataset.k);
const farben = Array.from(document.querySelectorAll(".vw-aufg-gruppe")).map((g) =>
getComputedStyle(g.querySelector(".vw-aufg-kopf")).color);
const balken = Array.from(document.querySelectorAll(".vw-fs-teil > i")).map((i) =>
getComputedStyle(i).backgroundColor);
const zeile = document.querySelector('[data-aufgabe="a2"]');
const normal = document.querySelector('[data-aufgabe="a5"]');
return {
gruppen, farben, balkenFarben: [...new Set(balken)].length,
kundenZeileAnders: zeile && normal
? getComputedStyle(zeile).backgroundColor !== getComputedStyle(normal).backgroundColor : false,
nichtEnthalten: document.querySelectorAll(".vw-aufg-nicht").length,
haken: document.querySelectorAll(".vw-aufg-haken").length,
erledigtDurchgestrichen: getComputedStyle(
document.querySelector('[data-aufgabe="a1"] .vw-aufg-titel')).textDecorationLine,
blickFelder: document.querySelectorAll(".vw-blick-feld").length,
/* Die REIHENFOLGE im Bild, nicht im Quelltext.
Diese Pruefung fehlte im ersten Anlauf, und deshalb meldete der
Test 37 von 37 gruen, waehrend jede Zeile sichtbar falsch herum
gebaut war: Kaestchen, dann die kleinen Knoepfe, und der Titel
ganz rechts. Alle inhaltlichen Pruefungen (Farben, Anzahl,
Durchgestrichenes) trafen zu -- keine einzige schaute darauf, WO
die Dinge liegen. Aufgefallen ist es erst am Screenshot.
Lehre: Wer eine Anordnung baut, muss die Anordnung pruefen, nicht
nur ihre Bestandteile. */
anordnung: (() => {
const z = document.querySelector('[data-aufgabe="a2"]');
if (!z) return null;
const k = z.querySelector(".vw-aufg-haken").getBoundingClientRect();
const t = z.querySelector(".vw-aufg-titel").getBoundingClientRect();
const r = z.querySelector(".vw-aufg-rechts").getBoundingClientRect();
const b = z.querySelector(".vw-aufg-text");
return {
reihenfolgeStimmt: k.right <= t.left + 1 && t.right <= r.left + 1,
textUnterTitel: b ? b.getBoundingClientRect().top >= t.bottom - 2 : true,
textNichtUnterKaestchen: b ? b.getBoundingClientRect().left >= k.right - 1 : true,
};
})(),
};
});
pruefe(wo + ": alle vier Abschnitte erscheinen", m.gruppen.join(",") === "start,gestaltung,umsetzung,abschluss", m.gruppen.join(","));
pruefe(wo + ": jeder Abschnitt hat eine EIGENE Farbe", new Set(m.farben).size === 4, m.farben.join(" | "));
pruefe(wo + ": Punkt für den Kunden hebt sich ab", m.kundenZeileAnders);
pruefe(wo + ": „nicht enthalten“ ist gekennzeichnet", m.nichtEnthalten === 1);
pruefe(wo + ": jeder Punkt hat ein Kästchen zum Abhaken", m.haken === 7, "n=" + m.haken);
pruefe(wo + ": Erledigtes ist durchgestrichen", m.erledigtDurchgestrichen.includes("line-through"), m.erledigtDurchgestrichen);
pruefe(wo + ": Zusammenfassung mit vier Zahlen", m.blickFelder === 4);
pruefe(wo + ": Kästchen — Titel — Knöpfe stehen in dieser Reihenfolge", m.anordnung?.reihenfolgeStimmt);
pruefe(wo + ": die Erklärung steht UNTER dem Titel", m.anordnung?.textUnterTitel);
pruefe(wo + ": die Erklärung ist am Titel ausgerichtet, nicht am Rand", m.anordnung?.textNichtUnterKaestchen);
/* ---- Stand ändern ----
Dafür gab es bis zu dieser Erweiterung KEINE Oberfläche. Der Kunde
sah Phase und "nächster Schritt" ganz oben in seinem Portal, und
beides liess sich nirgends ändern — ein Projekt blieb für immer im
Briefing stehen, egal wie weit es wirklich war. */
pruefe(wo + ": alle Phasen sind wählbar",
await seite.locator("[data-phase]").count() === 9, "7 Phasen + 2 Sonderfälle");
pruefe(wo + ": die aktuelle Phase ist hervorgehoben",
await seite.locator('[data-phase="design"].vw-an').count() === 1);
pruefe(wo + ": „wer ist am Zug“ zeigt den Kunden",
await seite.locator('[data-wartet="kunde"].vw-an').count() === 1);
gesendet.length = 0;
await seite.click('[data-phase="entwicklung"]');
await seite.waitForTimeout(400);
pruefe(wo + ": Phasenklick speichert sofort",
gesendet.some((g) => g && g.status === "entwicklung"), JSON.stringify(gesendet));
await seite.click('[data-wartet="dogfather"]');
await seite.waitForTimeout(400);
pruefe(wo + ": „wer ist am Zug“ speichert sofort",
gesendet.some((g) => g && g.wartetAuf === "dogfather"));
await seite.fill("#vw-p-schritt", "Entwurf ansehen und Rückmeldung geben");
await seite.fill("#vw-p-termin", "Mitte Oktober");
gesendet.length = 0;
await seite.click("#vw-p-speichern");
await seite.waitForTimeout(500);
pruefe(wo + ": „nächster Schritt“ und Termin werden gespeichert",
gesendet.some((g) => g && g.naechsterSchritt === "Entwurf ansehen und Rückmeldung geben"
&& g.richttermin === "Mitte Oktober"), JSON.stringify(gesendet));
gesendet.length = 0;
await seite.check("#vw-p-rest");
await seite.waitForTimeout(400);
pruefe(wo + ": Zahlungsstand wird sofort gespeichert",
gesendet.some((g) => g && g.restBezahlt === true), JSON.stringify(gesendet));
/* ---- Änderungswunsch beziffern ----
Der Kunde konnte seit jeher Ideen einreichen — beziffern ging nur
serverseitig. Der Kreis war offen: Er schickt etwas los und hört nie
wieder davon. */
pruefe(wo + ": beide Änderungswünsche erscheinen", await seite.locator("[data-ae]").count() === 2);
pruefe(wo + ": nur der OFFENE hat ein Bezifferungsfeld",
await seite.locator("[data-beziffern]").count() === 1);
// Ohne Preis darf nichts rausgehen — der Kunde könnte sonst nicht entscheiden.
beziffert = null;
await seite.click("[data-beziffern]");
await seite.waitForTimeout(400);
pruefe(wo + ": ohne Preis wird NICHT gesendet", beziffert === null, JSON.stringify(beziffert));
await seite.fill(".vw-ae-preis", "250");
await seite.fill(".vw-ae-dauer", "3 Tage");
await seite.click("[data-beziffern]");
await seite.waitForTimeout(600);
pruefe(wo + ": mit Preis geht das Angebot raus",
beziffert && beziffert.preisEuro === 250 && beziffert.dauer === "3 Tage", JSON.stringify(beziffert));
/* ---- Nachricht zum Projekt ----
Ohne sie hiess es: Der Kunde schreibt im Projekt, und ich kann ihm
nur woanders antworten. */
pruefe(wo + ": Projektnachrichten erscheinen",
await seite.locator("#vw-p-blasen .vw-blase").count() === 2);
await seite.fill("#vw-p-nachricht", "Der Entwurf kommt morgen.");
await seite.click("#vw-p-senden");
await seite.waitForTimeout(500);
pruefe(wo + ": Antwort zum Projekt geht raus",
projektNachricht && projektNachricht.text === "Der Entwurf kommt morgen."
&& projektNachricht.projektId === "p1", JSON.stringify(projektNachricht));
// Touch-Ziele auf dem Handy
if (breite < 900) {
const zuKlein = await seite.evaluate(() =>
Array.from(document.querySelectorAll(".vw-aufg-haken, .vw-mini"))
.filter((b) => { const r = b.getBoundingClientRect(); return r.width < 24 || r.height < 24; }).length);
pruefe(wo + ": alle Knöpfe sind gross genug zum Antippen", zuKlein === 0, "zu klein: " + zuKlein);
}
await seite.screenshot({ path: `pruef-aufgaben-${wo.toLowerCase()}.png` });
await seite.keyboard.press("Escape");
await seite.waitForTimeout(300);
// ---- Postfach ----
await seite.click('[data-tab="postfach"]');
await seite.waitForTimeout(500);
pruefe(wo + ": Postfach zeigt den Verlauf", await seite.locator("[data-verlauf]").count() === 1);
await seite.click("[data-verlauf]");
await seite.waitForTimeout(600);
const pf = await seite.evaluate(() => {
const intern = document.querySelector(".vw-blase--intern");
const wir = document.querySelector(".vw-blase--wir");
const kunde = document.querySelector(".vw-blase--kunde");
const lauf = document.getElementById("vw-blasen");
return {
/* Auf den Postfach-Verlauf eingegrenzt. Ein document-weites
".vw-blase" zaehlt auch die Nachrichten der Projektansicht mit --
die ist zwar geschlossen, steht aber weiter im Dokument. */
anzahl: document.querySelectorAll("#vw-blasen .vw-blase").length,
internDa: !!intern,
internAnders: intern && wir
? getComputedStyle(intern).borderStyle !== getComputedStyle(wir).borderStyle : false,
kundeLinks: kunde && wir
? kunde.getBoundingClientRect().left < wir.getBoundingClientRect().left : false,
amEnde: lauf ? lauf.scrollTop + lauf.clientHeight >= lauf.scrollHeight - 5 : false,
schalterDa: !!document.getElementById("vw-pf-intern"),
};
});
pruefe(wo + ": alle drei Nachrichten erscheinen", pf.anzahl === 3, "n=" + pf.anzahl);
pruefe(wo + ": die interne Notiz ist da", pf.internDa);
pruefe(wo + ": interne Notiz sieht ANDERS aus als eine gesendete", pf.internAnders);
pruefe(wo + ": Kunde links, eigene Nachrichten rechts", pf.kundeLinks);
pruefe(wo + ": Verlauf springt ans Ende", pf.amEnde);
// Der Schalter muss das Schreibfeld sichtbar umfaerben
const vorher = await seite.evaluate(() => getComputedStyle(document.getElementById("vw-schreibfeld")).borderColor);
await seite.check("#vw-pf-intern");
await seite.waitForTimeout(250);
const nachher = await seite.evaluate(() => getComputedStyle(document.getElementById("vw-schreibfeld")).borderColor);
pruefe(wo + ": „interne Notiz“ färbt das Schreibfeld sichtbar um", vorher !== nachher, vorher + " -> " + nachher);
pruefe(wo + ": keine Fehler im Protokoll", fehlerImProtokoll.length === 0, fehlerImProtokoll.join(" | "));
await seite.screenshot({ path: `pruef-postfach-${wo.toLowerCase()}.png` });
await seite.close();
}
await browser.close();
srv.close();
const fehler = ergebnisse.filter((e) => !e.ok).length;
console.log(`\n===== ${ergebnisse.length - fehler} bestanden, ${fehler} fehlgeschlagen =====`);
process.exitCode = fehler ? 1 : 0;