/* =====================================================================
Prueft die Buehne im Verwaltungsbereich.
Das Konzept hat sich umgedreht, und damit auch die Pruefung.
Vorher: ein schmaler Streifen, auf 190 % gezoomt, davon ein Ausschnitt
gewaehlt und die linke Haelfte voll zugedeckt -- damit Text darauf
lesbar bleibt. Gemessen wurde, ob das Bild leise genug ist. Uebrig
blieb ein Zipfel.
Jetzt: Das Motiv ist die Buehne -- bildschirmfuellend, ungeschnitten,
ungedimmt. Der Inhalt schwebt als Milchglas darueber. Text steht nie
direkt auf dem Bild.
Deshalb wird jetzt das Gegenteil geprueft:
1. Ist das Bild wirklich GANZ da? (kein Zoom, kein Ausschnitt)
2. Traegt jede Flaeche, auf der gelesen wird, auch Glas?
3. Bleibt der Text darauf lesbar -- an echten Bildpunkten gemessen?
===================================================================== */
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 : "")); }
};
function leuchtkraft([r, g, b]) {
const f = (v) => {
v /= 255;
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
};
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
}
function verhaeltnis(a, b) {
const l1 = leuchtkraft(a), l2 = leuchtkraft(b);
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}
const BEREICHE = ["uebersicht", "anfragen", "projekte", "kunden", "zahlungen", "postfach"];
/* Setzt einen Bereich und legt Beispielinhalt ein. Ohne Inhalt liesse
sich nicht messen, ob Text ueber der Buehne lesbar bleibt -- und genau
das ist die Frage, an der das ganze Konzept haengt.
Der Aufbau passiert vor JEDER Messung neu: Der Datenabruf laeuft im
Hintergrund weiter und klappt den Arbeitsbereich verzoegert wieder zu
(Zugangspruefung, kein Fehler). */
async function bereichAufbauen(seite, bereich) {
await seite.evaluate((b) => {
document.querySelector("#vw-anmeldung").setAttribute("hidden", "");
const bereichEl = document.querySelector("#vw-bereich");
bereichEl.removeAttribute("hidden");
document.documentElement.setAttribute("data-vw-bereich", b);
document.querySelectorAll("[data-tab]").forEach((x) =>
x.classList.toggle("vw-an", x.dataset.tab === b));
let probe = document.querySelector("#vw-probe");
if (!probe) {
probe = document.createElement("div");
probe.id = "vw-probe";
probe.innerHTML =
'
' +
'
Vanessa Geske
' +
'
vanessa@example.com
' +
'
Zwei Projekte, eines davon laufend.
';
bereichEl.appendChild(probe);
}
}, bereich);
await seite.waitForTimeout(700);
}
/* Misst die Schwankung des Untergrunds an echten Bildpunkten. Hohe
Schwankung heisst: Da liegt Bildstruktur unter der Schrift. */
async function unruheAn(seite, wahl) {
const bild = await seite.locator(wahl).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;
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 < 120) 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"));
}
srv.listen(4404, async () => {
const browser = await chromium.launch();
const seite = await browser.newPage({ viewport: { width: 1440, height: 900 } });
const skriptfehler = [];
seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 140)));
await seite.goto("http://127.0.0.1:4404/webdesign/verwaltung.html");
await seite.waitForTimeout(1500);
console.log("===== 1. IST DAS BILD GANZ DA? =====");
await bereichAufbauen(seite, "kunden");
const buehne = await seite.evaluate(() => {
const el = document.querySelector(".vw-buehne");
if (!el) return null;
const st = getComputedStyle(el);
const r = el.getBoundingClientRect();
const treffer = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i);
return {
datei: treffer ? treffer[1] : "(keins)",
groesse: st.backgroundSize,
lage: st.backgroundPosition,
haltung: st.position,
breite: Math.round(r.width),
hoehe: Math.round(r.height),
z: parseInt(st.zIndex, 10),
klicks: st.pointerEvents,
versteckt: el.getAttribute("aria-hidden"),
};
});
if (!buehne) { pruefe("Buehne vorhanden", false); }
else {
/* Das ist der Kern der Korrektur. Beim ersten Anlauf stand hier
"190% auto" plus ein Ausschnitt wie "84% 46%" -- damit war der
groesste Teil des Bildes weggeschnitten. */
pruefe("das Bild wird NICHT gezoomt", buehne.groesse === "cover", buehne.groesse);
pruefe("und NICHT einseitig ausgeschnitten",
/50%\s+50%|center/.test(buehne.lage), buehne.lage);
pruefe("die Buehne fuellt den ganzen Bildschirm",
buehne.breite >= 1430 && buehne.hoehe >= 890, `${buehne.breite}x${buehne.hoehe}`);
pruefe("sie steht fest, der Inhalt scrollt darueber",
buehne.haltung === "fixed", buehne.haltung);
pruefe("sie liegt hinter allem", buehne.z < 0, "z-index " + buehne.z);
pruefe("sie faengt keine Klicks ab", buehne.klicks === "none", buehne.klicks);
pruefe("sie ist als Zierde ausgezeichnet", buehne.versteckt === "true");
}
console.log("\n===== 2. TRAEGT ALLES GLAS, WORAUF GELESEN WIRD? =====");
const glas = await seite.evaluate(() => {
const pruefen = [".wd-karte", ".vw-filter-knopf", ".vw-wer"];
const raus = {};
for (const w of pruefen) {
const el = document.querySelector("#vw-bereich " + w);
if (!el) { raus[w] = "(nicht gefunden)"; continue; }
const st = getComputedStyle(el);
raus[w] = st.backdropFilter || st.webkitBackdropFilter || "keins";
}
return raus;
});
for (const [w, wert] of Object.entries(glas)) {
if (wert === "(nicht gefunden)") continue;
pruefe(` ${w} hat Rueckseiten-Unschaerfe`, /blur/.test(wert), wert.slice(0, 34));
}
console.log("\n===== 3. BLEIBT DER TEXT LESBAR? =====");
for (const bereich of BEREICHE) {
await bereichAufbauen(seite, bereich);
const aufGlas = await unruheAn(seite, "#vw-probe .wd-karte");
await seite.evaluate(() => {
document.querySelector(".vw-buehne").style.visibility = "hidden";
});
await seite.waitForTimeout(350);
await bereichAufbauen(seite, bereich);
const ohneBuehne = await unruheAn(seite, "#vw-probe .wd-karte");
await seite.evaluate(() => {
document.querySelector(".vw-buehne").style.visibility = "";
});
await seite.waitForTimeout(250);
const mehr = (aufGlas ?? 99) - (ohneBuehne ?? 0);
/* Etwas Struktur DARF durchkommen -- das ist ja der Reiz. Sie darf
nur nicht so stark werden, dass die Schrift darauf flimmert. */
pruefe(` ${bereich}: der Kachelgrund bleibt ruhig`,
aufGlas !== null && ohneBuehne !== null && mehr <= 8,
`mit Buehne ${aufGlas}, ohne ${ohneBuehne} → plus ${mehr}`);
await bereichAufbauen(seite, bereich);
/* Der tatsaechliche Kachelgrund wird aus dem BILD gelesen, nicht aus
dem Stilblatt: Die Kachel ist halbdurchsichtig, ihr rechnerischer
Wert sagt nichts darueber, was am Ende darunter liegt. */
const stelle = await seite.evaluate(() => {
const k = document.querySelector("#vw-probe .wd-karte");
const r = k.getBoundingClientRect();
return { x: Math.round(r.x + r.width * 0.72), y: Math.round(r.y + r.height * 0.62) };
});
const punkt = await seite.screenshot({
clip: { x: stelle.x, y: stelle.y, width: 10, height: 10 } });
const grundfarbe = await 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;
let r = 0, g = 0, b = 0, n = 0;
for (let i = 0; i < d.length; i += 4) { r += d[i]; g += d[i + 1]; b += d[i + 2]; n++; }
return [Math.round(r / n), Math.round(g / n), Math.round(b / n)];
}, punkt.toString("base64"));
const schrift = await seite.evaluate(() =>
getComputedStyle(document.querySelector("#vw-probe-text"))
.color.match(/[\d.]+/g).slice(0, 3).map(Number));
const v = verhaeltnis(schrift, grundfarbe);
pruefe(` ${bereich}: Text auf der Kachel erfuellt AA (4,5:1)`,
v >= 4.5, v.toFixed(2) + ":1");
}
console.log("\n===== 3b. FOLGT DAS LICHT DER MAUS? =====");
await bereichAufbauen(seite, "kunden");
const karte = seite.locator("#vw-probe .wd-karte");
const kasten = await karte.boundingBox();
/* WICHTIG: mit steps bewegen. Ein Sprung ohne Zwischenschritte
erzeugt kein pointermove -- der Test wuerde "Licht folgt nicht"
melden, obwohl alles stimmt. Diese Falle hat hier schon einmal
einen Fehlalarm ausgeloest. */
await seite.mouse.move(kasten.x + 30, kasten.y + 20, { steps: 8 });
await seite.waitForTimeout(320);
const lichtA = await karte.evaluate((e) => ({
x: getComputedStyle(e).getPropertyValue("--wd-lichtx").trim(),
y: getComputedStyle(e).getPropertyValue("--wd-lichty").trim(),
}));
await seite.mouse.move(kasten.x + kasten.width - 40,
kasten.y + kasten.height - 20, { steps: 16 });
await seite.waitForTimeout(400);
const lichtB = await karte.evaluate((e) => {
const st = getComputedStyle(e);
const na = getComputedStyle(e, "::after");
return {
x: st.getPropertyValue("--wd-lichtx").trim(),
y: st.getPropertyValue("--wd-lichty").trim(),
lumen: st.getPropertyValue("--wd-lumen").trim(),
kante: st.backgroundImage.includes("radial-gradient"),
kegel: na.backgroundImage,
};
});
pruefe("das Licht wandert mit dem Zeiger",
lichtA.x !== lichtB.x && lichtA.y !== lichtB.y,
lichtA.x + "/" + lichtA.y + " -> " + lichtB.x + "/" + lichtB.y);
pruefe("die Kachelkante leuchtet an der Zeigerstelle", lichtB.kante);
/* Der Lichtkegel muss die Leitfarbe des Bereichs tragen, nicht die
Grundfarbe -- sonst leuchtet im Kundenbereich Baby Blue statt
Indigo. */
pruefe("der Lichtkegel traegt die Leitfarbe",
/97,\s*92,\s*255/.test(lichtB.kegel), lichtB.lumen);
console.log("\n===== 3c. BEWEGT SICH DIE BUEHNE MIT? =====");
await seite.mouse.move(160, 780, { steps: 12 });
await seite.waitForTimeout(650);
const lage1 = await seite.evaluate(() =>
getComputedStyle(document.querySelector(".vw-buehne")).translate);
await seite.mouse.move(1320, 130, { steps: 20 });
await seite.waitForTimeout(750);
const lage2 = await seite.evaluate(() =>
getComputedStyle(document.querySelector(".vw-buehne")).translate);
pruefe("die Buehne verschiebt sich gegen den Zeiger",
lage1 !== lage2 && lage1 !== "none", lage1 + " -> " + lage2);
/* Der Ausschlag muss klein bleiben. Waere er gross, wuerde beim Lesen
staendig etwas im Augenwinkel wandern. */
const weite = Math.max(...String(lage2).match(/-?[\d.]+/g).map((n) => Math.abs(+n)));
pruefe("und zwar nur wenige Bildpunkte", weite <= 16, weite.toFixed(1) + "px");
console.log("\n===== 4. WECHSELT DER RAUM WIRKLICH? =====");
const gesehen = [];
for (const bereich of BEREICHE) {
await bereichAufbauen(seite, bereich);
const d = await seite.evaluate(() => {
const st = getComputedStyle(document.querySelector(".vw-buehne"));
const t = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i);
return {
datei: t ? t[1] : "(keins)",
farbe: getComputedStyle(document.documentElement)
.getPropertyValue("--vw-farbe").trim(),
};
});
gesehen.push({ bereich, ...d });
console.log(` ${bereich.padEnd(11)} ${d.datei.padEnd(22)} ${d.farbe}`);
}
pruefe("jeder Bereich hat eine eigene Leitfarbe",
new Set(gesehen.map((g) => g.farbe)).size === 6,
new Set(gesehen.map((g) => g.farbe)).size + " von 6");
pruefe("mindestens fuenf verschiedene Motive",
new Set(gesehen.map((g) => g.datei)).size >= 5,
new Set(gesehen.map((g) => g.datei)).size + " verschiedene");
pruefe("kein Skriptfehler", skriptfehler.length === 0, skriptfehler[0] || "");
await bereichAufbauen(seite, "kunden");
await seite.screenshot({ path: "pruef-vw-schmuck-computer.png" });
await seite.close();
console.log("\n===== 5. REDUZIERTE BEWEGUNG =====");
const ruhig = await browser.newPage({
viewport: { width: 1440, height: 900 }, reducedMotion: "reduce" });
await ruhig.goto("http://127.0.0.1:4404/webdesign/verwaltung.html");
await ruhig.waitForTimeout(1300);
await bereichAufbauen(ruhig, "projekte");
const bewegung = await ruhig.evaluate(() => {
const st = getComputedStyle(document.querySelector(".vw-buehne"));
return { animation: st.animationName, sichtbar: st.visibility };
});
pruefe("keine Animation bei reduzierter Bewegung",
bewegung.animation === "none", bewegung.animation);
/* Abschalten heisst NICHT verschwinden. */
pruefe("die Buehne ist trotzdem da", bewegung.sichtbar === "visible", bewegung.sichtbar);
await ruhig.mouse.move(200, 700, { steps: 10 });
await ruhig.waitForTimeout(400);
await ruhig.mouse.move(1200, 200, { steps: 16 });
await ruhig.waitForTimeout(600);
const ruhigeLage = await ruhig.evaluate(() => {
const b = document.querySelector(".vw-buehne");
const vor = getComputedStyle(b, "::before");
return {
translate: getComputedStyle(b).translate,
reflex: vor.animationName,
reflexSichtbar: parseFloat(vor.opacity),
};
});
pruefe("keine Parallaxe bei reduzierter Bewegung",
ruhigeLage.translate === "none", ruhigeLage.translate);
pruefe("und kein Lichtreflex",
ruhigeLage.reflex === "none" && ruhigeLage.reflexSichtbar === 0,
ruhigeLage.reflex + ", Deckkraft " + ruhigeLage.reflexSichtbar);
await ruhig.close();
console.log("\n===== 6. HANDY =====");
const handy = await browser.newPage({ viewport: { width: 390, height: 844 } });
await handy.goto("http://127.0.0.1:4404/webdesign/verwaltung.html");
await handy.waitForTimeout(1300);
await bereichAufbauen(handy, "zahlungen");
const hd = await handy.evaluate(() => {
const st = getComputedStyle(document.querySelector(".vw-buehne"));
const t = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i);
return { datei: t ? t[1] : "(keins)", groesse: st.backgroundSize };
});
pruefe("Handy: kleine Bildfassung", hd.datei.includes("-klein"), hd.datei);
pruefe("Handy: auch hier kein Zoom", hd.groesse === "cover", hd.groesse);
const ueberlauf = await handy.evaluate(() =>
document.documentElement.scrollWidth - document.documentElement.clientWidth);
pruefe("Handy: nichts laeuft seitlich ueber", ueberlauf <= 1, ueberlauf + "px");
await handy.screenshot({ path: "pruef-vw-schmuck-handy.png" });
await handy.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);
});