Der Feldfokus zeigt jetzt den Ion-Rand und den weichen Schein, den das
Design-System auf Seite 15 verlangt. Er fehlte, weil der allgemeine
Tastaturring seinen dunklen Innenring darueberlegte: Dessen Selektor hat
Spezifitaet 0,3,0, die Feldregel nur 0,2,1 -- und Spezifitaet schlaegt
Reihenfolge, egal wie weit unten die Regel steht. Eingabefelder sind
jetzt vom Innenring ausgenommen; sie brauchen ihn nicht, weil sie selbst
eine dunkle Flaeche sind. Der Aqua-Umriss bleibt fuer alle erhalten.
Die zwei Blickfang-Motive stehen jetzt auch wirklich auf einer Seite:
das Wappen auf "Ueber mich", direkt nach dem Absatz ueber die Herkunft
des Namens, der Monolith auf "Portfolio" zwischen Einleitung und den
echten Projekten. Beide als Zierbild ausgezeichnet -- ihre Aussage steht
schon im Text daneben. Auf dem Handy laedt automatisch die kleine
Fassung.
Vier Fehler steckten in der Pruefung selbst, nicht im Stilblatt:
- Sie griff das erste "input" der Anfrageseite. Das sind aber sechs
optisch versteckte Auswahl-Radios, kein Textfeld.
- Sie mass ohne Fensterfokus. Ein Browser wendet :focus nur an, wenn das
Fenster selbst den Fokus hat -- das DOM meldet trotzdem brav
matches(":focus") === true, die Farbe bleibt die alte.
- Sie mass mitten im Uebergang, bevor die Animation stand.
- Und sie zaehlte Schattenebenen an "),", einem Muster, das in keinem
Schattenwert je vorkommt. Geprueft wird jetzt die Anforderung selbst:
Ion-Farbe und ein Weichzeichner ueber 0.
Geprueft: 334 Pruefungen gruen (CRYONOVA 53, Blickfang 13, Abmelden 16,
Angebot 54, Abbruch 42, System 5, Automatik 55, Angebote 56, Kette 40).
Co-Authored-By: Claude Opus 5 <[email protected]>
525 lines
25 KiB
JavaScript
525 lines
25 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 } });
|
|
|
|
/* Ohne das misst jede :focus-Pruefung den Ruhezustand. Ein Browser
|
|
wendet :focus nur an, wenn das FENSTER selbst den Fokus hat -- im
|
|
Testlauf hat es den nie. Das DOM meldet trotzdem brav
|
|
element.matches(":focus") === true, die Farbe bleibt aber die alte.
|
|
Genau diese Kombination hat hier zwei Fehlalarme erzeugt. */
|
|
const cdp = await seite.context().newCDPSession(seite);
|
|
await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true });
|
|
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. */
|
|
/* AUSGENOMMEN ist die Druckansicht: Auf Papier IST Weiss richtig,
|
|
und dunkles Navy zu drucken waere Toner-Verschwendung und
|
|
schlecht lesbar. Das Verbot des Systems gilt dem Bildschirm.
|
|
Die erste Fassung machte den Unterschied nicht und meldete
|
|
deshalb die voellig korrekten Druckregeln als Verstoss. */
|
|
keinWeiss: !Array.from(document.styleSheets)
|
|
.flatMap((b) => { try { return Array.from(b.cssRules); } catch { return []; } })
|
|
.filter((r) => !/print/.test(r.conditionText || (r.media && r.media.mediaText) || ""))
|
|
.map((r) => r.cssText).join("\n")
|
|
.match(/background:\s*(#fff|#ffffff|white)\b/i),
|
|
};
|
|
});
|
|
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");
|
|
|
|
console.log("\n=== 12. SIGNATURE-VERLAEUFE (Seite 18) ===");
|
|
await seite.goto("http://127.0.0.1:4372/webdesign/index.html");
|
|
await seite.waitForTimeout(400);
|
|
|
|
const verlaeufe = await seite.evaluate(() => {
|
|
const alle = Array.from(document.styleSheets)
|
|
.flatMap((b) => { try { return Array.from(b.cssRules); } catch { return []; } });
|
|
const text = alle.map((r) => r.cssText).join("\n");
|
|
const gefunden = ["cryo", "prisma", "aurora", "liquid"]
|
|
.filter((n) => text.includes(".wd-verlauf--" + n));
|
|
|
|
/* Champagne NIEMALS mit Violet mischen (Seite 18). Das ist keine
|
|
Geschmacksfrage: Champagne steht fuer Treue und Bezahltes, Violet
|
|
fuer Spezielles. Gemischt kann niemand mehr ablesen, was eine
|
|
Flaeche bedeutet -- und Ablesbarkeit ist der Zweck des Systems. */
|
|
const liquid = alle.find((r) => (r.selectorText || "").includes("wd-verlauf--liquid"));
|
|
const liquidText = liquid ? liquid.cssText : "";
|
|
return {
|
|
gefunden,
|
|
champagneRein: !/--wd-lila-rgb|--wd-indigo-rgb/.test(liquidText),
|
|
};
|
|
});
|
|
pruefe("alle vier Signature-Verlaeufe sind gebaut",
|
|
verlaeufe.gefunden.length === 4, verlaeufe.gefunden.join(", "));
|
|
pruefe("Liquid Value mischt Champagne NICHT mit Violet",
|
|
verlaeufe.champagneRein, "Verbot aus Seite 18 eingehalten");
|
|
|
|
console.log("\n=== 13. FORMULARE UND TABELLEN (Seite 15) ===");
|
|
await seite.goto("http://127.0.0.1:4372/webdesign/anfrage.html");
|
|
await seite.waitForTimeout(600);
|
|
|
|
/* Echter Klick statt evaluate-focus(), und danach warten: Der Rand hat
|
|
einen Uebergang. Sofort gemessen sieht man die Animation auf halbem
|
|
Weg -- Rand noch alt, Schein noch bei 0 0 0 0 und durchsichtig.
|
|
|
|
Der Selektor nennt ausdruecklich Texteingaben: Die Anfrageseite
|
|
beginnt mit sechs Auswahlkacheln, deren Radios optisch versteckt
|
|
sind. Ein blindes "erstes input" greift die statt des Feldes. */
|
|
await seite.locator('.wd-feld textarea, .wd-feld input[type="text"], .wd-eingabe')
|
|
.first().click();
|
|
await seite.waitForTimeout(700);
|
|
|
|
const feldFokus = await seite.evaluate(() => {
|
|
/* Nur ECHTE Texteingaben. Die Anfrageseite beginnt mit sechs
|
|
Auswahlkacheln, deren Radios optisch versteckt sind -- ein blindes
|
|
"erstes input" greift die und misst deren neutrales Weiss statt des
|
|
Ion-Randes. Genau das hat hier zweimal falschen Alarm ausgeloest. */
|
|
const f = document.activeElement;
|
|
if (!f || f === document.body) return null;
|
|
const st = getComputedStyle(f);
|
|
return { rand: st.borderColor, schatten: st.boxShadow };
|
|
});
|
|
/* Das System nennt ausdruecklich "Ion-Blue-Rand und sanften Glow". */
|
|
pruefe("das Eingabefeld bekommt im Fokus einen Ion-Rand",
|
|
feldFokus && /74,\s*200,\s*255/.test(feldFokus.rand), feldFokus && feldFokus.rand);
|
|
/* Geprueft wird die Anforderung selbst, nicht die Anzahl Kommas:
|
|
Der Schein muss in Ion-Blue leuchten und WEICH sein, also einen
|
|
Weichzeichner ueber 0 haben. Eine harte 0px-Kante waere ein Rahmen,
|
|
kein Schein. (Die frueher hier stehende Zaehlung trennte an "),"
|
|
-- das Muster kommt in keinem Schattenwert je vor, weil nach der
|
|
Farbe immer noch die Masse folgen.) */
|
|
const scheinIon = feldFokus && /74,\s*200,\s*255/.test(feldFokus.schatten);
|
|
const scheinWeich = feldFokus &&
|
|
[...feldFokus.schatten.matchAll(/74,\s*200,\s*255[^)]*\)((?:\s+-?\d+px){2,4})/g)]
|
|
.some((t) => parseFloat(t[1].trim().split(/\s+/)[2]) > 0);
|
|
pruefe("und einen weichen Schein dazu",
|
|
scheinIon && scheinWeich,
|
|
feldFokus && feldFokus.schatten.slice(0, 60));
|
|
|
|
const tabelle = 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");
|
|
/* "sehr schwaches, positionsabhaengiges Licht" -- die Betonung liegt
|
|
auf SCHWACH. Eine Tabelle ist zum Lesen da; ein kraeftiges
|
|
Wanderlicht zieht den Blick zur Maus statt zur Zahl. */
|
|
const m = alle.match(/wd-tabelle tbody tr:hover[^}]*rgb\(var\(--wd-blau-rgb\)\s*\/\s*\.(\d+)\)/);
|
|
return { gebaut: alle.includes(".wd-tabelle"), staerke: m ? Number("0." + m[1]) : null,
|
|
positionsabhaengig: /wd-tabelle tbody tr:hover[^}]*--wd-lichtx/.test(alle) };
|
|
});
|
|
pruefe("Tabellen sind gestaltet", tabelle.gebaut);
|
|
pruefe("das Zeilenlicht ist positionsabhaengig", tabelle.positionsabhaengig);
|
|
pruefe("und bleibt sehr schwach (unter 10 %)",
|
|
tabelle.staerke !== null && tabelle.staerke < 0.10,
|
|
tabelle.staerke !== null ? Math.round(tabelle.staerke * 100) + " %" : "nicht gefunden");
|
|
|
|
console.log("\n=== 14. KACHEL-ANATOMIE (Seite 11) ===");
|
|
await seite.goto("http://127.0.0.1:4372/webdesign/leistungen.html");
|
|
await seite.waitForTimeout(600);
|
|
const anatomie = await seite.evaluate(() => {
|
|
const k = document.querySelector(".wd-karte");
|
|
if (!k) return null;
|
|
const st = getComputedStyle(k);
|
|
return {
|
|
ebenen: (st.backgroundImage.match(/gradient/g) || []).length,
|
|
lichtkante: /inset/.test(st.boxShadow),
|
|
randbreite: st.borderWidth,
|
|
};
|
|
});
|
|
/* Die Kachel baut ihre Kante ueber eine ZWEITE Hintergrundebene auf der
|
|
border-box -- der Rahmen selbst ist durchsichtig. Wer beim Anpassen
|
|
nur die Flaeche setzt, laesst die Kante ersatzlos wegfallen: Genau
|
|
das ist beim Bauen passiert, der Rand mass danach rgba(0,0,0,0).
|
|
Sichtbar war es kaum, messbar sofort. */
|
|
pruefe("die Kachel hat zwei Hintergrundebenen (Flaeche UND Kante)",
|
|
anatomie && anatomie.ebenen >= 2, anatomie && anatomie.ebenen);
|
|
pruefe("sie hat eine innere Lichtkante", anatomie && anatomie.lichtkante);
|
|
pruefe("die Kante ist 1 px stark (System Seite 11)",
|
|
anatomie && anatomie.randbreite === "1px", anatomie && anatomie.randbreite);
|
|
|
|
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;
|