/* ===================================================================== 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;