/* ===================================================================== Prueft den Bildschmuck im Verwaltungsbereich. Die eigentliche Frage ist nicht "sieht man das Bild?", sondern: Kostet es Lesbarkeit? Die Verwaltung ist ein Arbeitsplatz -- ein Motiv, das hinter einer Zahlenspalte liegt, ist genau die Art von Schoenheit, die ein Werkzeug unbrauchbar macht. ===================================================================== */ import { chromium } from "playwright"; import http from "http"; import fs from "fs"; import path from "path"; const TYP = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript", ".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp", ".json": "application/json", ".ico": "image/x-icon", ".jpg": "image/jpeg", }; const fehlt = []; const srv = http.createServer((q, r) => { const u = decodeURIComponent(q.url.split("?")[0]); const p = path.join(process.cwd(), u); if (fs.existsSync(p) && !p.endsWith("/")) { r.writeHead(200, { "content-type": TYP[path.extname(p)] || "text/plain" }); return r.end(fs.readFileSync(p)); } fehlt.push(u); r.writeHead(404); r.end(""); }); let ok = 0, fehl = 0; const pruefe = (name, gut, info) => { if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); } else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); } }; srv.listen(4404, async () => { const browser = await chromium.launch(); for (const [gr, vp] of [["Computer", { width: 1440, height: 950 }], ["Handy", { width: 390, height: 844 }]]) { console.log(`\n===== ${gr} =====`); const seite = await browser.newPage({ viewport: vp }); const skriptfehler = []; seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 120))); await seite.goto("http://127.0.0.1:4404/webdesign/verwaltung.html"); await seite.waitForTimeout(1500); /* ---- Der Schmuckstreifen ---- */ const streifen = await seite.evaluate(() => { const el = document.querySelector(".vw-schmuck"); if (!el) return null; const s = getComputedStyle(el); const r = el.getBoundingClientRect(); const treffer = s.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i); return { datei: treffer ? treffer[1] : "(keins)", deckkraft: parseFloat(s.opacity), z: parseInt(s.zIndex, 10), oben: Math.round(r.top), hoehe: Math.round(r.height), klicks: s.pointerEvents, versteckt: el.getAttribute("aria-hidden"), }; }); if (!streifen) { pruefe(`${gr}: Schmuckstreifen vorhanden`, false); continue; } const kleinErwartet = vp.width <= 900; pruefe(`${gr}: richtige Bilddatei`, kleinErwartet === streifen.datei.includes("-klein"), streifen.datei); /* Das ist der Kern: Der Streifen darf ANGEDEUTET sein, nicht mehr. */ pruefe(`${gr}: bleibt sehr leise (unter 20 %)`, streifen.deckkraft > 0 && streifen.deckkraft < 0.20, Math.round(streifen.deckkraft * 100) + " %"); pruefe(`${gr}: liegt hinter dem Inhalt`, streifen.z < 0, "z-index " + streifen.z); pruefe(`${gr}: faengt keine Klicks ab`, streifen.klicks === "none", streifen.klicks); pruefe(`${gr}: ist als Zierde ausgezeichnet`, streifen.versteckt === "true"); /* 67px ist kein Zufallswert, sondern die Hoehe der Kopfleiste: Der Streifen beginnt direkt darunter. Gemessen wird deshalb gegen die tatsaechliche Kopfhoehe, nicht gegen eine geratene Zahl. */ const kopfHoehe = await seite.evaluate(() => { const k = document.querySelector("#wd-kopf .wd-kopf, #wd-kopf header, #wd-kopf"); return k ? Math.round(k.getBoundingClientRect().height) : 0; }); pruefe(`${gr}: sitzt direkt unter der Kopfleiste`, streifen.oben <= kopfHoehe + 8, `${streifen.oben}px, Kopf ${kopfHoehe}px`); /* ---- Die Anmeldekarte traegt das Wappen ---- */ const login = await seite.evaluate(() => { const el = document.querySelector("#vw-anmeldung"); if (!el) return null; el.removeAttribute("hidden"); // sonst nichts zu messen const vor = getComputedStyle(el, "::before"); const treffer = vor.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i); return { klassen: el.className, datei: treffer ? treffer[1] : "(keins)", deckkraft: parseFloat(vor.opacity), }; }); pruefe(`${gr}: die Anmeldekarte hat ein Motiv`, login && login.datei.includes("wappen"), login && login.datei); /* Der eigentliche Test. Deckkraft allein sagt nichts: Ein Motiv mit hellen Kanten ist bei 20 % stoerender als ein ruhiges bei 50 %. Was zaehlt, ist wie stark der Untergrund UNTER DER SCHRIFT schwankt. Gemessen wird das an echten Bildpunkten aus dem Erklaertext -- genau dort lag beim Bauen der Hundekopf, und die Zeilen liefen quer ueber Schnauze und Schriftzug. */ await seite.waitForTimeout(500); const messen = async () => { const bild = await seite.locator("#vw-anmeldung p.wd-klein").first().screenshot(); return seite.evaluate(async (daten) => { const bl = await fetch("data:image/png;base64," + daten).then((r) => r.blob()); const bm = await createImageBitmap(bl); const c = new OffscreenCanvas(bm.width, bm.height); const x = c.getContext("2d"); x.drawImage(bm, 0, 0); const d = x.getImageData(0, 0, bm.width, bm.height).data; /* Nur die dunklen Bildpunkte: Die hellen SIND die Schrift, die soll ja kontrastieren. Interessant ist der Grund dazwischen. */ const grund = []; for (let i = 0; i < d.length; i += 4) { const h = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2]; if (h < 110) grund.push(h); } if (grund.length < 50) return null; const m = grund.reduce((a, b) => a + b, 0) / grund.length; return Math.round(Math.sqrt(grund.reduce((a, b) => a + (b - m) ** 2, 0) / grund.length)); }, bild.toString("base64")); }; const mitBild = await messen(); /* Die Vergleichsmessung ohne Motiv ist der Kern. Ein fester Grenzwert waere geraten: Schon die weichgezeichneten Kanten der Buchstaben allein erzeugen eine Schwankung um 12 -- ein Test gegen "unter 14" haette also fast nur diese Kanten gemessen und waere je nach Schriftgroesse zufaellig gruen oder rot. Gefragt ist, wie viel Unruhe DAS MOTIV hinzufuegt. */ await seite.evaluate(() => document.querySelector("#vw-anmeldung").classList.remove("wd-motiv", "wd-motiv--wappen")); await seite.waitForTimeout(400); const ohneBild = await messen(); await seite.evaluate(() => document.querySelector("#vw-anmeldung").classList.add("wd-motiv", "wd-motiv--wappen")); await seite.waitForTimeout(400); const zusatz = (mitBild ?? 99) - (ohneBild ?? 0); pruefe(`${gr}: das Motiv macht den Textgrund kaum unruhiger`, mitBild !== null && ohneBild !== null && zusatz <= 5, `mit ${mitBild}, ohne ${ohneBild} → plus ${zusatz}`); /* ---- Und jetzt die eigentliche Regel ---- */ const stoerung = await seite.evaluate(() => { const schlimm = []; for (const el of document.querySelectorAll("table, .wd-tabelle, .vw-liste")) { let p = el; while (p && p !== document.body) { if (p.classList && (p.classList.contains("wd-motiv") || p.classList.contains("vw-schmuck"))) { schlimm.push(el.tagName + "." + (el.className || "").slice(0, 20)); break; } p = p.parentElement; } } return schlimm; }); pruefe(`${gr}: kein Motiv hinter Tabellen oder Listen`, stoerung.length === 0, stoerung.join(", ") || "keine"); pruefe(`${gr}: kein Skriptfehler`, skriptfehler.length === 0, skriptfehler[0] || ""); await seite.screenshot({ path: `pruef-vw-schmuck-${gr.toLowerCase()}.png` }); await seite.close(); } const verpasst = fehlt.filter((f) => /\.(webp|jpg|png|css|js|svg)$/.test(f)); pruefe("keine fehlende Datei", verpasst.length === 0, verpasst.join(", ") || "keine"); console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`); await browser.close(); srv.close(); process.exit(fehl ? 1 : 0); });