/* Prüft die neue Detailansicht der Verwaltung im echten Browser. Die Seite spricht mit dem internen Dienst, den es hier nicht gibt. Deshalb werden alle Netzanfragen abgefangen und mit erfundenen, aber REALISTISCHEN Daten beantwortet — insbesondere einer sehr knappen Anfrage, denn genau die sah vorher schlecht aus (halbe Ansicht "keine Angabe"). Geprüft wird nicht "sieht gut aus", sondern messbar: - stehen die Spalten wirklich nebeneinander - ist die rechte Spalte nicht breiter als die linke - ragt nichts aus dem Fenster - ist die Bildlaufleiste dunkel statt weiss */ import { chromium } from "playwright"; import { createServer } from "node:http"; import { readFileSync, existsSync } from "node:fs"; import { join, extname } from "node:path"; const WURZEL = process.cwd(); const TYPEN = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml", ".webp": "image/webp", ".ico": "image/x-icon" }; const server = createServer((req, res) => { const pfad = join(WURZEL, decodeURIComponent(req.url.split("?")[0])); if (existsSync(pfad) && !pfad.endsWith("/")) { res.writeHead(200, { "content-type": TYPEN[extname(pfad)] || "text/plain" }); return res.end(readFileSync(pfad)); } res.writeHead(404).end("nix"); }); await new Promise((r) => server.listen(4321, r)); const ANFRAGE_KNAPP = { id: "t1", nummer: "A-2608-0002", name: "Selbsttest", email: "selbsttest@example.com", telefon: null, kontaktweg: "email", sprache: "de", paket: "onepager", ziel: "TESTANFRAGE aus dem Selbsttest - bitte archivieren.", zielgruppe: null, seiten: null, funktionen: [], details: {}, hat_texte: null, hat_bilder: null, hat_logo: null, hat_domain: null, budget: null, termin: null, bemerkung: null, status: "neu", archiviert: 0, kunde_id: null, erstellt_am: new Date(Date.now() - 3 * 86400000).toISOString(), }; const browser = await chromium.launch(); const ergebnisse = []; function pruefe(name, ok, zusatz) { ergebnisse.push({ name, ok, zusatz }); console.log((ok ? " ok " : " FEHL ") + name + (zusatz ? " -> " + zusatz : "")); } for (const [bezeichnung, breite, hoehe] of [["Computer", 1440, 900], ["Handy", 390, 844]]) { const seite = await browser.newPage({ viewport: { width: breite, height: hoehe } }); await seite.route("**/webdesign/admin/**", (route) => { const u = route.request().url(); if (u.includes("/anfragen/t1")) { return route.fulfill({ json: { ok: true, anfrage: ANFRAGE_KNAPP, notizen: [] } }); } if (u.includes("/anfragen")) { return route.fulfill({ json: { ok: true, anfragen: [ANFRAGE_KNAPP] } }); } /* Die Uebersicht ist seit dem 23.08.2026 die Startansicht und wird bei jedem Laden abgerufen. Ohne eigene Ersatzantwort landet sie im Auffang unten, bekommt ein blankes {ok:true} und der Aufbau bricht ab, bevor irgendetwas geprueft werden kann. */ if (u.includes("/uebersicht")) { return route.fulfill({ json: { ok: true, beiMir: { anfragen: { n: 1 }, nachrichten: { n: 0 }, wuensche: { n: 0 }, widerrufe: { n: 0 }, projekte: 0 }, beiKunde: { projekte: 0, aufgaben: 0, zahlungen: { n: 0, summe: 0 }, zahlungenText: "0,00 €" }, geld: { monat: { n: 0, brutto: 0, netto: 0, bruttoText: "0,00 €", nettoText: "0,00 €" }, gesamt: { n: 0, bruttoText: "0,00 €" }, gebuehrenText: "0,00 €" }, projekte: [], verlauf: [], bestand: {} } }); } return route.fulfill({ json: { ok: true, kunden: [], projekte: [] } }); }); await seite.route("**/auth/**", (r) => r.fulfill({ json: { ok: true, user: { name: "Test" } } })); await seite.addInitScript(() => { sessionStorage.setItem("wd-vw-token", "testtoken"); sessionStorage.setItem("wd-sitzung-aktiv", "1"); }); const fehlerImProtokoll = []; seite.on("pageerror", (e) => fehlerImProtokoll.push(e.message)); await seite.goto("http://127.0.0.1:4321/webdesign/verwaltung.html"); await seite.waitForTimeout(900); console.log("\n=== " + bezeichnung + " (" + breite + "x" + hoehe + ") ==="); /* Erst in die Anfragen wechseln. Die Verwaltung oeffnet seit dem 23.08.2026 im Cockpit, nicht mehr in der Anfragenliste -- ohne diesen Klick sucht der Test eine Karte, die es noch gar nicht gibt. */ await seite.click('[data-tab="anfragen"]'); await seite.waitForTimeout(600); const karte = seite.locator(".vw-karte").first(); if (await karte.count()) { await karte.click(); await seite.waitForTimeout(700); } const da = await seite.locator(".vw-detail-spalten").count(); pruefe(bezeichnung + ": Detailansicht öffnet sich", da === 1); if (!da) { await seite.close(); continue; } const mass = await seite.evaluate(() => { const li = document.querySelector(".vw-dt-links"); const re = document.querySelector(".vw-dt-rechts"); const blatt = document.querySelector(".vw-detail-blatt"); const leere = document.querySelectorAll(".vw-ohne"); const blick = document.querySelectorAll(".vw-blick-feld"); const keineAngabe = Array.from(document.querySelectorAll(".vw-paar dd.vw-leer")).length; return { li: li?.getBoundingClientRect(), re: re?.getBoundingClientRect(), blatt: blatt?.getBoundingClientRect(), leereZeilen: leere.length, blickFelder: blick.length, keineAngabe, fensterBreite: innerWidth, fensterHoehe: innerHeight, ueberlauf: blatt ? blatt.scrollWidth > blatt.clientWidth + 1 : false, }; }); if (breite >= 900) { pruefe(bezeichnung + ": Spalten stehen NEBENeinander", Math.abs(mass.li.top - mass.re.top) < 40, `links y=${Math.round(mass.li.top)}, rechts y=${Math.round(mass.re.top)}`); pruefe(bezeichnung + ": rechte Spalte ist schmaler als die linke", mass.re.width < mass.li.width, `${Math.round(mass.li.width)} vs ${Math.round(mass.re.width)}`); pruefe(bezeichnung + ": Spalten überlappen sich nicht", mass.li.right <= mass.re.left + 1, `links endet ${Math.round(mass.li.right)}, rechts beginnt ${Math.round(mass.re.left)}`); } else { pruefe(bezeichnung + ": Spalten stehen UNTEReinander", mass.re.top > mass.li.top, `links y=${Math.round(mass.li.top)}, rechts y=${Math.round(mass.re.top)}`); } pruefe(bezeichnung + ": Fenster ragt nicht über den Bildschirm", mass.blatt.left >= -1 && mass.blatt.right <= mass.fensterBreite + 1, `${Math.round(mass.blatt.left)} … ${Math.round(mass.blatt.right)} bei ${mass.fensterBreite}`); pruefe(bezeichnung + ": kein waagerechtes Schieben nötig", !mass.ueberlauf); pruefe(bezeichnung + ": „Auf einen Blick“ zeigt vier Felder", mass.blickFelder === 4, "n=" + mass.blickFelder); pruefe(bezeichnung + ": leere Angaben sind zusammengefasst", mass.leereZeilen >= 3, "n=" + mass.leereZeilen); pruefe(bezeichnung + ": keine einzelne „keine Angabe“-Zeile mehr", mass.keineAngabe === 0, "n=" + mass.keineAngabe); pruefe(bezeichnung + ": keine Fehler im Protokoll", fehlerImProtokoll.length === 0, fehlerImProtokoll.join(" | ")); await seite.screenshot({ path: `pruef-detail-${bezeichnung.toLowerCase()}.png`, fullPage: false }); await seite.close(); } /* Bildlaufleiste: dunkel statt weiss? */ const s2 = await browser.newPage({ viewport: { width: 1200, height: 700 } }); await s2.goto("http://127.0.0.1:4321/webdesign/verwaltung.html"); await s2.waitForTimeout(400); const bl = await s2.evaluate(() => { const stil = getComputedStyle(document.body); const d = document.createElement("div"); d.style.cssText = "overflow:auto;height:40px"; d.innerHTML = "
"; document.body.appendChild(d); return { farbschema: stil.colorScheme, hatRegel: !!Array.from(document.styleSheets) .flatMap((s) => { try { return Array.from(s.cssRules); } catch { return []; } }) .find((r) => r.selectorText && r.selectorText.includes("scrollbar-thumb")) }; }); console.log("\n=== Bildlaufleiste ==="); pruefe("color-scheme steht auf dark (Firefox/Safari)", bl.farbschema === "dark", bl.farbschema); pruefe("eigene Regel für den Griff vorhanden (Chrome/Edge)", bl.hatRegel); await s2.close(); await browser.close(); server.close(); const fehler = ergebnisse.filter((e) => !e.ok).length; console.log(`\n===== ${ergebnisse.length - fehler} bestanden, ${fehler} fehlgeschlagen =====`); process.exitCode = fehler ? 1 : 0;