Files
dogfather-universe/pruef-cryonova.mjs
T
DogFatherGit e92daed103 CRYONOVA Schritt 2: Bildwelt, Materialien, Rangsystem, Status
Setzt die restlichen Kapitel des Design-Systems um (Seiten 7, 14, 16,
19, 24-29). Wie Schritt 1 ausschliesslich Farbe, Material und Licht --
Aufbau, Navigation, Texte und Funktionen bleiben unangetastet.

BILDWELT (Seiten 24-29)
Die Motive sind jetzt echte Seitenhintergruende: Ring auf Startseite
und Portal, Kristallwelt an der Zugangswand. Ueber jedem Motiv liegt
eine Navy-Glasflaeche -- das System fordert auf Seite 31 ausdruecklich,
dass bei Bedarf "eine Navy- oder Frost-Glasflaeche zwischen Bild und
Inhalt" liegt. Ohne sie waere Text auf den hellen Kristallspitzen nicht
sicher lesbar, und genau dort macht ein schoenes Bild eine Seite
unbrauchbar.

Von 2,5 MB auf 96-229 KB (WebP), auf dem Handy 30-73 KB. Das System
nennt fuer Mobile ausdruecklich Ladezeit als Kriterium.

DAS LOGO IM BILD -- eine Entscheidung gegen die woertliche Vorgabe
Zwei der drei Motive zeigen das Dogfather-Logo gross in der Mitte. Als
Seitenhintergrund waere das ein zweites Logo neben dem echten in der
Kopfzeile: zwei Marken, die um dieselbe Aufmerksamkeit ringen. Beim
ersten Versuch schien es an der Zugangswand hinter den Karten durch
(gemessen 15,9 % helle Flaeche im Logobereich). Die Zugangswand nutzt
deshalb jetzt einen Ausschnitt der linken Bildhaelfte -- dieselbe
Kristallwelt, dasselbe Licht, nur ohne Wappen. Die Logo-Motive bleiben
als Blickfang-Klasse erhalten, dort wo sie fuer sich stehen duerfen.

MATERIALIEN (Seite 7)
Optic Glass, Frosted Ice, Prism Edge und Void Navy als Klassen. "Hell"
heisst auf einer dunklen Seite aufgehelltes Navy, nicht Weiss -- echtes
Weiss waere ein Loch im Bildschirm, und das System verbietet weisse
Vollflaechen ausdruecklich.

RANGSYSTEM (Seite 14)
Drei Stufen fehlten: Premium (Indigo), VIP (Champagne), Gefahr (Coral).
Wenn jeder Knopf gleich laut ist, ist keiner mehr laut.

Zwei bewusste Abweichungen von der naheliegenden Loesung:
- Indigo steht als FLAECHE unter hellem Text, nie als Schriftfarbe. Es
  erreicht als Text nirgends 4,5:1 (gemessen 3,25-4,29). Der Test haelt
  das dauerhaft fest.
- Gefahr ist NICHT vollflaechig rot. Ein voll gefuellter roter Knopf
  zieht den Blick staerker an als die Hauptaktion und wird dadurch
  versehentlich gedrueckt. Kritische Aktionen sollen auffindbar sein,
  nicht verlockend.

FOKUSRING (Seite 19)
Doppelter Aqua-Ring: 2 px Aqua aussen, dunkler Innenring. Kein Schmuck
-- ein einfacher heller Ring verschwindet auf hellen Flaechen, ein
dunkler auf dunklen. Die Kombination ist auf JEDEM Untergrund sichtbar.

STATUS-SPEKTRUM (Seite 16)
Acht Zustaende. Die Klasse faerbt nur -- den Text liefert immer das
Markup. Es gibt bewusst keine Variante, die nur einen farbigen Punkt
zeigt: Wer Farben nicht unterscheiden kann, saehe dann gar nichts. Der
Test prueft, dass keine Statusmarke ohne Text existiert.

Dabei einen eigenen Fehler gefunden und behoben: Ich hatte beim Bauen
des Premium-Knopfes selbst einen losen Hex-Wert eingesetzt -- genau
das, was Token-Regel 05 verbietet und was der Test dann meldete.

Geprueft: 43 (CRYONOVA, von 24 erweitert) + 0 Fundstellen (WCAG ueber
12 Seiten x 5 Sprachen, jetzt MIT Bildhintergruenden) + 55 + 40 + 69 +
54 + 42 + 16 + 5 + 20 + 64 + 58 -- alles gruen.
2026-08-24 22:43:58 +02:00

398 lines
18 KiB
JavaScript

/* =====================================================================
pruef-cryonova.mjs — das Farb- und Lichtsystem
Prüft, dass die Seite dem Design-System "Baby Blue Optical Luxury"
(Edition 2.0) folgt — und zwar an den Stellen, wo ein Abweichen
folgenlos aussieht, aber Bedeutung zerstört:
1. Die Paletten-Werte selbst (ein Tippfehler in einem Hex-Code
fällt mit blossem Auge nicht auf).
2. Das Mauslicht MUSS erhalten bleiben und die Kachelfarbe
übernehmen — das System schützt es auf fünf Seiten
ausdrücklich vor Entfernen, Ersetzen und Abschwächen.
3. Keine losen Hex-Codes ausserhalb der Token (Token-Regel 05).
4. Kontraste — inklusive der Falle, an der ich beim Bauen selbst
hereingefallen bin (siehe unten).
DIE KONTRAST-FALLE, DIE HIER BEWUSST RICHTIG GERECHNET WIRD:
Ein erster, schneller Messversuch las den halbtransparenten
Kachelhintergrund als fertige Farbe und meldete beim aktiven Reiter
1:1 — also scheinbar unsichtbaren Text. In Wahrheit liegt die 13-%-
Fläche über einem dunklen Grund, und der echte Wert ist 8,25 bis
10,23:1. Wer halbtransparente Flächen nicht über ihren Untergrund
rechnet, bekommt Fehlalarme und stumpft irgendwann gegen die
Meldungen ab.
===================================================================== */
import { chromium } from "playwright";
import { createServer } from "node:http";
import { readFileSync, existsSync, readdirSync } 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(4372, r));
const ergebnisse = [];
const pruefe = (n, ok, z) => {
ergebnisse.push({ n, ok });
console.log((ok ? " ok " : " FEHL ") + n + (z !== undefined ? " -> " + z : ""));
};
/* Die verbindliche Palette (Seite 5 des Systems).
BEWUSST NICHT aus der Token-Liste auf Seite 20 übernommen: Dort sind
Namen und Hex-Werte um eine Zeile verrutscht. Am folgenreichsten
wäre --cryo-baby auf #0C2740 gewesen, einem fast schwarzen Navy —
und dieser Wert ist laut Seite 21 zugleich die Standard-Lumenfarbe.
Das Mauslicht wäre damit unsichtbar geworden, also genau der Effekt,
den das System an anderer Stelle als unantastbar bezeichnet. */
const PALETTE = {
"--wd-blau-rgb": { hex: "#8FD8FF", name: "Signature Baby" },
"--wd-ion-rgb": { hex: "#4AC8FF", name: "Ion Blue" },
"--wd-aqua-rgb": { hex: "#2EF2E0", name: "Hyper Aqua" },
"--wd-lila-rgb": { hex: "#A76BFF", name: "Aurora Violet" },
"--wd-indigo-rgb": { hex: "#615CFF", name: "Prism Indigo" },
"--wd-gold-rgb": { hex: "#E8C36A", name: "Liquid Gold" },
"--wd-fehler-rgb": { hex: "#FF5C7A", name: "Signal Coral" },
"--wd-chrome-rgb": { hex: "#AFC4D2", name: "Chrome Silver" },
};
const FLAECHEN = {
"--wd-schwarz": "#030810",
"--wd-dunkelblau": "#061323",
"--wd-dunkelblau-2": "#091B2D",
"--wd-flaeche": "#0C2740",
};
const hexZuRgb = (h) => [1, 3, 5].map((i) => parseInt(h.substr(i, 2), 16));
const browser = await chromium.launch();
const seite = await browser.newPage({ viewport: { width: 1440, height: 950 } });
await seite.goto("http://127.0.0.1:4372/webdesign/index.html");
await seite.waitForTimeout(500);
console.log("\n=== 1. PALETTE ===");
const werte = await seite.evaluate((namen) => {
const w = getComputedStyle(document.documentElement);
const raus = {};
for (const n of namen) raus[n] = w.getPropertyValue(n).trim();
return raus;
}, [...Object.keys(PALETTE), ...Object.keys(FLAECHEN)]);
for (const [token, soll] of Object.entries(PALETTE)) {
const ist = (werte[token] || "").split(/\s+/).map(Number);
const erwartet = hexZuRgb(soll.hex);
pruefe(`${soll.name} (${token}) ist ${soll.hex}`,
ist.length === 3 && ist.every((v, i) => v === erwartet[i]),
werte[token] + " statt " + erwartet.join(" "));
}
for (const [token, soll] of Object.entries(FLAECHEN)) {
pruefe(`Grundflaeche ${token} ist ${soll}`,
(werte[token] || "").toLowerCase() === soll.toLowerCase(), werte[token]);
}
console.log("\n=== 2. MAUSLICHT (darf nie entfernt werden) ===");
const lumen = await seite.evaluate(() => {
const css = Array.from(document.styleSheets)
.flatMap((b) => { try { return Array.from(b.cssRules); } catch { return []; } });
const alle = css.map((r) => r.cssText).join("\n");
return {
hatVariable: /--wd-lumen\s*:/.test(alle),
hatPosition: /--wd-lichtx/.test(alle) && /--wd-lichty/.test(alle),
nutztVariableImVerlauf: /radial-gradient\([^)]*var\(--wd-lichtx\)[^}]*var\(--wd-lumen\)/.test(alle)
|| /var\(--wd-lumen\)[^}]*var\(--wd-lichtx\)/.test(alle),
rollen: ["--wd-karte--lila", "aqua", "premium", "vip", "kritisch"]
.filter((r) => alle.includes(r)).length,
hatUebergang: /transition:\s*opacity/.test(alle),
hinterInhalt: /\.wd-karte\s*>\s*\*\s*\{[^}]*z-index:\s*1/.test(alle),
};
});
pruefe("die Lichtposition wird als Variable gefuehrt", lumen.hatPosition);
pruefe("es gibt eine Farbvariable pro Kachel (--wd-lumen)", lumen.hatVariable);
pruefe("der Lichtverlauf benutzt diese Farbvariable", lumen.nutztVariableImVerlauf);
pruefe("mehrere Lumen-Rollen sind hinterlegt", lumen.rollen >= 4, lumen.rollen + " gefunden");
pruefe("das Licht blendet weich aus (Uebergang vorhanden)", lumen.hatUebergang);
/* Das System verlangt ausdruecklich: Licht hinter dem Inhalt, niemals
darueber -- sonst leidet die Lesbarkeit (Seite 9). */
pruefe("der Inhalt liegt UEBER dem Licht", lumen.hinterInhalt);
console.log("\n=== 3. LICHT FOLGT WIRKLICH DEM ZEIGER ===");
await seite.goto("http://127.0.0.1:4372/webdesign/leistungen.html");
await seite.waitForTimeout(600);
const karte = seite.locator(".wd-karte").first();
if (await karte.count()) {
const box = await karte.boundingBox();
/* MIT ZWISCHENSCHRITTEN bewegen (steps), nicht in einem Sprung.
Der Code haengt an "pointermove". Ein Sprung von A nach B erzeugt
unter Umstaenden nur ein einziges Ereignis am Ziel -- und die erste
Messung sah dann zweimal denselben Wert und meldete faelschlich,
das Licht folge dem Zeiger nicht. Eine direkte Nachmessung zeigte:
Es folgt einwandfrei (30px -> 723px). Der Fehler lag in der
Nachahmung der Mausbewegung, nicht in der Seite. */
await seite.mouse.move(box.x + 40, box.y + 30, { steps: 5 });
await seite.waitForTimeout(250);
const p1 = await karte.evaluate((e) => e.style.getPropertyValue("--wd-lichtx"));
/* Deckkraft JETZT messen, solange der Zeiger noch auf der Kachel
steht -- danach wandert er weiter und das Licht blendet aus. */
const sichtbar = await karte.evaluate((e) =>
getComputedStyle(e, "::after").opacity);
await seite.mouse.move(box.x + box.width - 60, box.y + box.height - 40, { steps: 8 });
await seite.waitForTimeout(250);
const p2 = await karte.evaluate((e) => e.style.getPropertyValue("--wd-lichtx"));
pruefe("die Lichtposition folgt dem Zeiger",
p1 !== p2 && p1 !== "" && p2 !== "", `${p1} -> ${p2}`);
pruefe("das Licht ist beim Ueberfahren voll sichtbar",
parseFloat(sichtbar) > 0.5, "Deckkraft " + sichtbar);
pruefe("die Kachel gibt dem Licht ihre eigene Farbe",
(await karte.evaluate((e) => getComputedStyle(e).getPropertyValue("--wd-lumen"))).trim()
=== "143 216 255",
await karte.evaluate((e) => getComputedStyle(e).getPropertyValue("--wd-lumen")));
} else {
pruefe("eine Kachel zum Pruefen gefunden", false, "keine .wd-karte");
}
console.log("\n=== 4. KEINE LOSEN HEX-CODES (Token-Regel 05) ===");
/* Farben gehoeren in die Token. Verstreute Hex-Codes sind der Grund,
warum Oberflaechen mit der Zeit zwei fast gleiche Blautoene bekommen. */
const cssText = readFileSync(join(W, "assets/css/webdesign.css"), "utf8");
const ohneKommentare = cssText.replace(/\/\*[\s\S]*?\*\//g, "");
const wurzelEnde = ohneKommentare.indexOf("}", ohneKommentare.indexOf(":root"));
const nachWurzel = ohneKommentare.slice(wurzelEnde);
const lose = (nachWurzel.match(/#[0-9a-fA-F]{6}\b/g) || []);
pruefe("ausserhalb der Token stehen keine Hex-Farben",
lose.length === 0, lose.slice(0, 6).join(", ") || "keine");
console.log("\n=== 5. KONTRASTE (halbtransparente Flaechen korrekt gerechnet) ===");
const kontraste = await seite.evaluate(() => {
/* Halbtransparente Flaechen ueber ihren Untergrund legen, statt sie
als fertige Farbe zu lesen -- sonst kommen Fehlalarme heraus. */
function zahlen(c) { return (c.match(/[\d.]+/g) || []).map(Number); }
function ueberlagern(el) {
let p = el, stapel = [];
while (p) {
const c = zahlen(getComputedStyle(p).backgroundColor);
if (c.length >= 3) {
const a = c.length === 4 ? c[3] : 1;
if (a > 0) stapel.push([c[0], c[1], c[2], a]);
if (a >= 1) break;
}
p = p.parentElement;
}
let r = [3, 8, 16];
for (let i = stapel.length - 1; i >= 0; i--) {
const [cr, cg, cb, a] = stapel[i];
r = [cr * a + r[0] * (1 - a), cg * a + r[1] * (1 - a), cb * a + r[2] * (1 - a)];
}
return r;
}
function lum(c) {
const v = c.map((x) => { x /= 255; return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); });
return 0.2126 * v[0] + 0.7152 * v[1] + 0.0722 * v[2];
}
function kon(a, b) { const l1 = lum(a), l2 = lum(b); const [h, n] = l1 > l2 ? [l1, l2] : [l2, l1]; return +((h + 0.05) / (n + 0.05)).toFixed(2); }
const schlecht = [];
for (const el of document.querySelectorAll("p, h1, h2, h3, h4, a, span, li, label, button, td, th")) {
const t = (el.textContent || "").trim();
if (!t || el.children.length > 0) continue;
const r = el.getBoundingClientRect();
if (r.width < 2 || r.height < 2) continue;
/* AUSSERHALB DES BILDES -- kein sichtbarer Text.
Der Sprunglink ("Direkt zum Inhalt") liegt bei left:-9999px und
wird erst beim Tastaturfokus hereingeholt. Er ist technisch
sichtbar, aber niemand liest ihn dort. */
if (r.right < 0 || r.left > innerWidth) continue;
const st = getComputedStyle(el);
if (st.visibility === "hidden" || st.display === "none" || +st.opacity < 0.1) continue;
/* TEXT AUF EINEM FARBVERLAUF laesst sich nicht mit einer einzigen
Hintergrundfarbe verrechnen -- getComputedStyle liefert dort
rgba(0,0,0,0), und die Messung faellt auf den Seitenhintergrund
zurueck. Beim Hauptknopf (dunkler Text auf hellem Babyblau-
Verlauf) kam so 1:1 heraus, obwohl er in Wirklichkeit rund 11:1
hat. Solche Faelle pruefe ich von Hand; pruef-design.mjs meldet
sie getrennt als "von Hand zu pruefen". */
let ahn = el, aufVerlauf = false;
for (let i = 0; ahn && i < 4; i++, ahn = ahn.parentElement) {
if (getComputedStyle(ahn).backgroundImage !== "none") { aufVerlauf = true; break; }
}
if (aufVerlauf) continue;
const gr = parseFloat(st.fontSize);
const fett = +st.fontWeight >= 700;
const ziel = (gr >= 24 || (gr >= 18.66 && fett)) ? 3 : 4.5;
const k = kon(zahlen(st.color).slice(0, 3), ueberlagern(el));
if (k < ziel) schlecht.push({ t: t.slice(0, 30), k, ziel, gr });
}
return schlecht;
});
pruefe("jeder sichtbare Text erreicht seinen Zielkontrast",
kontraste.length === 0,
kontraste.slice(0, 4).map((x) => `"${x.t}" ${x.k}/${x.ziel}`).join(" | ") || "alle OK");
console.log("\n=== 6. DAS SYSTEM WIRKT AUF ALLEN SEITEN ===");
/* Eine zentrale Quelle heisst: eine Aenderung wirkt ueberall. Wenn eine
Seite noch einen alten Ton zeigt, ist genau das nicht der Fall. */
const seiten = readdirSync(join(W, "webdesign")).filter((f) => f.endsWith(".html") && f !== "diagnose.html");
let abweichend = [];
for (const datei of seiten) {
await seite.goto("http://127.0.0.1:4372/webdesign/" + datei);
await seite.waitForTimeout(180);
const baby = await seite.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue("--wd-blau-rgb").trim());
if (baby !== "143 216 255") abweichend.push(datei + ": " + baby);
}
pruefe(`alle ${seiten.length} Seiten nutzen dieselbe Farbquelle`,
abweichend.length === 0, abweichend.join(", ") || "einheitlich");
console.log("\n=== 7. AKTIONEN MIT RANG (Seite 14) ===");
await seite.goto("http://127.0.0.1:4372/webdesign/index.html");
await seite.waitForTimeout(400);
const raenge = await seite.evaluate(() => {
const alle = Array.from(document.styleSheets)
.flatMap((b) => { try { return Array.from(b.cssRules); } catch { return []; } })
.map((r) => r.cssText).join("\n");
return ["haupt", "zweit", "still", "premium", "vip", "gefahr"]
.filter((r) => alle.includes(".wd-btn--" + r));
});
pruefe("alle sechs Rangstufen sind hinterlegt", raenge.length === 6, raenge.join(", "));
/* Prism Indigo erreicht als SCHRIFT nirgends 4,5:1 (3,25-4,29 gemessen).
Es darf deshalb nur als Flaeche unter hellem Text vorkommen. Diese
Pruefung haelt genau das fest -- sonst rutscht die Farbe beim
naechsten Feinschliff doch in eine Textregel. */
const indigoAlsText = await seite.evaluate(() => {
const alle = Array.from(document.styleSheets)
.flatMap((b) => { try { return Array.from(b.cssRules); } catch { return []; } })
.filter((r) => r.style && r.style.color)
.filter((r) => /--wd-indigo/.test(r.style.color));
return alle.map((r) => r.selectorText);
});
pruefe("Prism Indigo wird NIE als Schriftfarbe benutzt",
indigoAlsText.length === 0, indigoAlsText.join(", ") || "keine");
console.log("\n=== 8. MATERIALIEN (Seite 7) ===");
const materialien = await seite.evaluate(() => {
const alle = Array.from(document.styleSheets)
.flatMap((b) => { try { return Array.from(b.cssRules); } catch { return []; } })
.map((r) => r.cssText).join("\n");
return {
glas: alle.includes(".wd-glasflaeche"),
frost: alle.includes(".wd-frost"),
prisma: alle.includes(".wd-prisma"),
void_: alle.includes(".wd-void"),
/* Das System verbietet weisse Vollflaechen ausdruecklich (Seite 23
und 31). Auf einer dunklen Seite waere echtes Weiss ein Loch. */
keinWeiss: !/background:\s*(#fff|#ffffff|white)\b/i.test(alle),
};
});
pruefe("Optic Glass ist gebaut", materialien.glas);
pruefe("Frosted Ice ist gebaut", materialien.frost);
pruefe("Prism Edge ist gebaut", materialien.prisma);
pruefe("Void Navy ist gebaut", materialien.void_);
pruefe("keine weisse Vollflaeche als Hintergrund", materialien.keinWeiss);
console.log("\n=== 9. STATUS: NIE NUR FARBE (Seite 16) ===");
const status = await seite.evaluate(() => {
const alle = Array.from(document.styleSheets)
.flatMap((b) => { try { return Array.from(b.cssRules); } catch { return []; } })
.map((r) => r.cssText).join("\n");
return ["neu", "arbeit", "wartet", "freigabe", "bezahlt", "kritisch", "pausiert", "vip"]
.filter((n) => alle.includes(".wd-status--" + n));
});
pruefe("alle acht Zustaende sind hinterlegt", status.length === 8, status.length + " von 8");
/* Der Kern der Regel: Eine Statusmarke muss Text enthalten. Eine
Variante, die nur einen farbigen Punkt zeigt, waere fuer jeden
unlesbar, der Farben nicht unterscheiden kann. */
const statusOhneText = await seite.evaluate(() => {
const raus = [];
for (const el of document.querySelectorAll("[class*='wd-status--'], [class*='vw-status--'], .po-frist")) {
if (!(el.textContent || "").trim()) raus.push(el.className);
}
return raus;
});
pruefe("keine Statusmarke ohne Text", statusOhneText.length === 0,
statusOhneText.join(", ") || "alle mit Text");
console.log("\n=== 10. FOKUSRING (Seite 19) ===");
const fokus = await seite.evaluate(() => {
const el = document.querySelector("a[href], button");
if (!el) return null;
el.focus();
const st = getComputedStyle(el);
return { breite: st.outlineWidth, abstand: st.outlineOffset, farbe: st.outlineColor };
});
pruefe("der Fokusring ist mindestens 2px breit",
fokus && parseFloat(fokus.breite) >= 2, fokus && fokus.breite);
pruefe("er hat sichtbaren Abstand zum Element",
fokus && parseFloat(fokus.abstand) >= 2, fokus && fokus.abstand);
pruefe("er ist in Aqua gehalten",
fokus && /46,\s*242,\s*224/.test(fokus.farbe), fokus && fokus.farbe);
console.log("\n=== 11. BILDWELT (Seiten 24-29) ===");
/* Das System verlangt: keine leere weisse Vollflaeche, jede Seite
zeigt ein Motiv oder eine erkennbare Ableitung. Geprueft wird, dass
die Motive wirklich laden -- ein Tippfehler im Dateinamen faellt
sonst erst live auf. */
for (const [seitenname, datei] of [
["Startseite", "index.html"],
["Zugangswand", "zugang.html"],
["Portal", "portal.html"],
]) {
await seite.goto("http://127.0.0.1:4372/webdesign/" + datei);
await seite.waitForTimeout(500);
const motiv = await seite.evaluate(() => {
const el = document.querySelector(".wd-motiv");
if (!el) return null;
const vor = getComputedStyle(el, "::before");
return { bild: vor.backgroundImage, deckkraft: vor.opacity };
});
pruefe(`${seitenname}: ein Motiv ist gesetzt`,
motiv && /cryo-/.test(motiv.bild), motiv ? motiv.bild.slice(0, 50) : "keins");
/* Das Motiv darf den Text nicht ueberstrahlen -- deshalb gedaempft. */
pruefe(`${seitenname}: das Motiv ist gedaempft, nicht voll`,
motiv && parseFloat(motiv.deckkraft) < 0.7, motiv && motiv.deckkraft);
}
/* Die Bilddateien muessen wirklich existieren und klein genug sein.
Ein 2,5-MB-Hintergrund waere auf dem Handy inakzeptabel -- das
System nennt fuer Mobile ausdruecklich Ladezeit als Kriterium. */
/* cryo-kristall ist der logofreie Ausschnitt fuer die Zugangswand --
siehe Begruendung im Stilblatt: zwei Logos auf einem Bildschirm
waeren keine Wirkung, sondern ein Fehler. */
const bilder = ["cryo-ring", "cryo-wappen", "cryo-monolith", "cryo-kristall"];
let zuGross = [];
for (const b of bilder) {
for (const variante of [b + ".webp", b + "-klein.webp"]) {
const pfad = join(W, "assets/img", variante);
if (!existsSync(pfad)) { zuGross.push(variante + " FEHLT"); continue; }
const kb = readFileSync(pfad).length / 1024;
if (kb > 300) zuGross.push(variante + " " + Math.round(kb) + " KB");
}
}
pruefe("alle Motive sind vorhanden und unter 300 KB",
zuGross.length === 0, zuGross.join(", ") || "alle schlank");
await browser.close();
srv.close();
const f = ergebnisse.filter((e) => !e.ok).length;
console.log(`\n===== ${ergebnisse.length - f} bestanden, ${f} fehlgeschlagen =====`);
process.exitCode = f ? 1 : 0;