/* ===================================================================== pruef-uebersicht.mjs — die neue Startansicht der Verwaltung Zwei Dinge werden hier geprüft, und beide haben eine Vorgeschichte: 1. STEHEN DIE REITER WIRKLICH UNTER DEM TITEL? Das ist der ausdrückliche Wunsch vom 23.08.2026. Es wäre bequem, dafür das Vorhandensein einer CSS-Regel zu prüfen — das beweist aber nichts. Am 22.08.2026 waren 37 von 37 Prüfungen grün, während jede Aufgabenzeile im Browser verkehrt herum stand: Die Regeln waren da, das Ergebnis war falsch. Deshalb wird hier die tatsächliche Lage im Fenster gemessen — der Reiter MUSS weiter unten liegen als der Titel. 2. FÜHREN DIE KACHELN AUCH WIRKLICH IRGENDWOHIN? Eine Kachel, die aussieht wie ein Knopf und keiner ist, ist schlimmer als gar kein Knopf. Also wird geklickt und geprüft, dass danach die richtige Ansicht offen ist. ===================================================================== */ 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(4337, r)); const ergebnisse = []; function pruefe(name, ok, zusatz) { ergebnisse.push({ name, ok }); console.log((ok ? " ok " : " FEHL ") + name + (zusatz !== undefined ? " -> " + zusatz : "")); } const vorTagen = (n) => new Date(Date.now() - n * 864e5).toISOString(); /* Absichtlich unbequeme Testdaten: eine Anfrage, die seit acht Tagen liegt (muss rot werden), eine Nachricht seit drei Tagen (gelb), und ein offener Widerruf von heute (muss trotzdem rot sein, weil eine gesetzliche Frist läuft). Ein Datensatz, in dem alles in Ordnung ist, würde die Warnstufen gar nicht erst auslösen. */ const UEBERSICHT = { ok: true, beiMir: { anfragen: { n: 3, aeltestes: vorTagen(8) }, nachrichten: { n: 2, aeltestes: vorTagen(3) }, wuensche: { n: 0, aeltestes: null }, widerrufe: { n: 1, aeltestes: vorTagen(0) }, projekte: 2, }, beiKunde: { projekte: 1, aufgaben: 4, zahlungen: { n: 2, summe: 120000, aeltestes: vorTagen(5) }, zahlungenText: "1.200,00 €", }, geld: { monat: { n: 3, brutto: 250000, netto: 241000, bruttoText: "2.500,00 €", nettoText: "2.410,00 €" }, gesamt: { n: 11, brutto: 980000, bruttoText: "9.800,00 €" }, gebuehrenText: "90,00 €", }, projekte: [ { id: "p1", nummer: "P-2026-001", titel: "Website für Musterbau", status: "bau", wartet_auf: "dogfather", richttermin: "2026-09-15", kunde_name: "Anna Muster", aufg_gesamt: 20, aufg_fertig: 13 }, { id: "p2", nummer: "P-2026-002", titel: "Shop für Beispiel GmbH", status: "bau", wartet_auf: "kunde", richttermin: null, kunde_name: "Bert Beispiel", aufg_gesamt: 18, aufg_fertig: 4 }, ], verlauf: [ { bereich: "projekt", aktion: "Aufgabe erledigt: Startseite gebaut", wer: "dogfather", erstellt_am: vorTagen(0) }, { bereich: "zahlung", aktion: "Zahlung eingegangen", wer: "paypal", erstellt_am: vorTagen(1) }, ], bestand: { kunden: 5, projekte: 2, anfragenGesamt: 9 }, paypal: { eingerichtet: true, live: true }, }; 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)); await seite.route("**/webdesign/admin/**", (route) => { const u = new URL(route.request().url()).pathname; if (u.endsWith("/admin/uebersicht")) return route.fulfill({ json: UEBERSICHT }); if (u.includes("/admin/postfach-zaehler")) return route.fulfill({ json: { ok: true, ungelesen: 2 } }); if (u.includes("/admin/anfragen")) return route.fulfill({ json: { ok: true, anfragen: [] } }); if (u.includes("/admin/kunden")) return route.fulfill({ json: { ok: true, kunden: [] } }); if (u.includes("/admin/projekte")) return route.fulfill({ json: { ok: true, projekte: [] } }); if (u.includes("/admin/zahlungen")) return route.fulfill({ json: { ok: true, zahlungen: [] } }); if (u.includes("/admin/postfach")) return route.fulfill({ json: { ok: true, ungelesen: 2, verlaeufe: [] } }); 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:4337/webdesign/verwaltung.html"); await seite.waitForTimeout(900); console.log("\n=== " + wo + " ==="); /* ---- 1. Die Verwaltung startet in der Übersicht ---- */ const titel = await seite.locator("#vw-titel").innerText(); pruefe(wo + ": die Verwaltung öffnet in der Übersicht", titel.trim() === "Übersicht", titel); pruefe(wo + ": das Cockpit ist sichtbar", await seite.locator("#vw-ub-inhalt").isVisible()); pruefe(wo + ": die Anfragenliste liegt NICHT darunter", !(await seite.locator("#vw-liste").isVisible())); /* ---- 2. Reiter UNTER dem Titel — gemessen, nicht behauptet ---- */ const lage = await seite.evaluate(() => { const t = document.getElementById("vw-titel").getBoundingClientRect(); /* Gemessen wird der erste KNOPF, nicht der Streifen um ihn herum. Auf dem Handy sind die Reiter ein Wischstreifen, der bewusst 4px ueber den Containerrand hinauslaeuft -- sonst endet er optisch mitten auf der Seite, obwohl es weitergeht. Sichtbar ausgerichtet ist aber der Knopf, und nur der zaehlt fuer das Auge. */ const r = document.querySelector(".vw-tabs [data-tab]").getBoundingClientRect(); return { titelUnten: t.bottom, titelLinks: t.left, reiterOben: r.top, reiterLinks: r.left }; }); pruefe(wo + ": die Reiter beginnen UNTERHALB des Titels", lage.reiterOben >= lage.titelUnten - 1, "Titel endet bei " + Math.round(lage.titelUnten) + ", Reiter beginnen bei " + Math.round(lage.reiterOben)); /* Zusatzbedingung, sonst wäre "darunter" auch erfüllt, wenn die Reiter weit nach rechts eingerückt stünden statt am Seitenrand zu beginnen. */ pruefe(wo + ": die Reiter beginnen am selben linken Rand wie der Titel", Math.abs(lage.reiterLinks - lage.titelLinks) < 3, "Titel " + Math.round(lage.titelLinks) + " vs. Reiter " + Math.round(lage.reiterLinks)); /* ---- 2b. Groessenverhaeltnisse ---- Eine Zwischenueberschrift darf nicht groesser sein als die Zahl, um die es geht. Genau das war der Fall, solange die Regel .wd h2 die eigene ueberstimmte: 44px Ueberschrift ueber 33px Zahl -- die Seite las sich wie ein Plakat statt wie ein Arbeitsplatz. */ const groessen = await seite.evaluate(() => ({ h: parseFloat(getComputedStyle(document.querySelector(".vw-ub-h")).fontSize), zahl: parseFloat(getComputedStyle(document.querySelector(".vw-ub-zahl")).fontSize), })); pruefe(wo + ": die Zwischenueberschrift ist kleiner als die Zahl darunter", groessen.h < groessen.zahl, "Ueberschrift " + groessen.h + "px, Zahl " + groessen.zahl + "px"); /* ---- 3. Inhalt der Kacheln ---- */ const kacheln = await seite.locator("#vw-ub-beimir .vw-ub-kachel").allInnerTexts(); pruefe(wo + ": alle fünf Kacheln 'bei mir' sind da (inkl. Widerruf)", kacheln.length === 5, kacheln.length); pruefe(wo + ": die drei offenen Anfragen stehen drin", kacheln.some((k) => k.includes("3") && k.includes("offene Anfragen"))); pruefe(wo + ": das Alter steht als TEXT dabei, nicht nur als Farbe", kacheln.some((k) => k.includes("seit 8 Tagen")), kacheln.find((k) => k.includes("Anfragen"))); /* ---- 4. Die Warnstufen greifen wirklich ---- */ const stufen = await seite.evaluate(() => { const raus = {}; document.querySelectorAll("#vw-ub-beimir .vw-ub-kachel").forEach((k) => { const t = (k.querySelector(".vw-ub-titel") || {}).textContent || ""; raus[t] = k.className; }); return raus; }); pruefe(wo + ": 8 Tage alte Anfrage ist rot", /vw-ub-kachel--rot/.test(stufen["offene Anfragen"] || ""), stufen["offene Anfragen"]); pruefe(wo + ": 3 Tage alte Nachricht ist nur gelb, nicht rot", /--warn/.test(stufen["ungelesene Nachrichten"] || "") && !/--rot/.test(stufen["ungelesene Nachrichten"] || ""), stufen["ungelesene Nachrichten"]); pruefe(wo + ": ein Widerruf von HEUTE ist trotzdem rot (Frist läuft)", /--rot/.test(stufen["Widerrufe unbearbeitet"] || ""), stufen["Widerrufe unbearbeitet"]); pruefe(wo + ": eine Null-Kachel nimmt sich zurück statt zu warnen", /--leer/.test(stufen["Änderungswünsche zu prüfen"] || ""), stufen["Änderungswünsche zu prüfen"]); /* ---- 5. Geld ---- */ const geld = (await seite.locator("#vw-ub-geld").innerText()); pruefe(wo + ": der Monatsumsatz steht da", geld.includes("2.500,00")); pruefe(wo + ": die PayPal-Gebühr wird offen ausgewiesen", geld.includes("90,00")); pruefe(wo + ": das tatsächlich angekommene Netto steht dabei", geld.includes("2.410,00")); /* ---- 6. Projekte mit Fortschritt ---- */ const proj = await seite.locator("#vw-ub-projekte .vw-ub-projekt").allInnerTexts(); pruefe(wo + ": beide laufenden Projekte erscheinen", proj.length === 2, proj.length); pruefe(wo + ": der Fortschritt steht als Zahl da, nicht nur als Balken", proj[0].includes("13 von 20") && proj[0].includes("65"), proj[0].replace(/\n/g, " | ")); pruefe(wo + ": man sieht, wer am Zug ist", proj[0].includes("du bist dran") && proj[1].includes("wartet auf Kunden")); const balken = await seite.evaluate(() => document.querySelector(".vw-ub-balken-voll").style.width); pruefe(wo + ": der Balken zeigt denselben Wert wie der Text", balken === "65%", balken); /* ---- 7. Die Kacheln sind echte Knöpfe und führen irgendwohin ---- */ const istKnopf = await seite.evaluate(() => document.querySelector("#vw-ub-beimir .vw-ub-kachel").tagName); pruefe(wo + ": eine Kachel ist ein