/* =================================================================== Pruefung der Buehne (Casper und HasiDog im Hintergrund). Ein Bild hinter einem Arbeitswerkzeug ist der schnellste Weg zu einer Oberflaeche, die gut aussieht und unbenutzbar ist. Deshalb wird hier NICHT geprueft, ob es huebsch ist, sondern ob der Text noch lesbar ist -- und zwar an echten BILDPUNKTEN aus dem fertigen Bild, nicht an der Farbangabe im Stil. Die sagt naemlich nichts darueber, was dahinterliegt. Massstab WCAG: 4,5:1 fuer normalen Text, 3:1 fuer grossen. =================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-buehne-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); process.env.PORT = "4193"; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; const express = (await import("express")).default; const ec = express.response.cookie; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; await import("./index.js"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4193"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 220_000).unref?.(); let fehler = 0; const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); /* Warten, bis der Server die Datenbank wirklich angelegt hat. Der Anmeldeversuch oben STOESST das nur an -- Schema und Umstellungen laufen danach. Wer sofort weiterschreibt, trifft auf "no such table". */ for (let i = 0; i < 60; i++) { try { const p = new DatabaseSync(process.env.WORKSPACE_DB); try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } } catch (f) { if (i === 0 || i === 5) console.log(" warte auf die Datenbank:", f.message); await new Promise((r) => setTimeout(r, 250)); } } const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); const salt = randomBytes(16).toString("hex"); const hash = scryptSync("CODE-DOGI-0001", salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") .run("Filipe", "admin", hash, salt, 32768, jetzt); d.close(); const { chromium } = await import( "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await chromium.launch(); const SEITEN = ["start.html", "uebersicht.html", "personen.html", "content.html", "aufgaben.html"]; for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"]]) { console.log(`\n=== ${wie} (${breite}x${hoehe})`); const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } }); const seite = await kontext.newPage(); await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); await seite.click('.rolle[data-rolle="admin"]'); await seite.fill("#code", "CODE-DOGI-0001"); await seite.click("button[type=submit]"); await seite.waitForURL("**/start.html", { timeout: 15000 }); for (const s of SEITEN) { await seite.goto(BASIS + "/workspace/" + s, { waitUntil: "networkidle" }); await seite.waitForTimeout(800); const da = await seite.evaluate(() => ({ husky: /buehne-husky/.test(getComputedStyle(document.body, "::before").backgroundImage), hasi: /buehne-hasi/.test(getComputedStyle(document.body, "::after").backgroundImage), schleier: !!document.querySelector(".schleier"), durchlaessig: getComputedStyle(document.querySelector(".schleier") || document.body).pointerEvents, })); if (s === "start.html") { ok(da.husky, "Casper liegt im Hintergrund"); ok(da.hasi, "HasiDog liegt im Hintergrund"); } ok(da.schleier && da.durchlaessig === "none", `${s}: Schleier da und durchlässig für Klicks (${da.durchlaessig})`); /* Der eigentliche Punkt: Kontrast an ECHTEN Bildpunkten aus dem fertigen Bild. Gemessen wird der Untergrund direkt NEBEN jedem sichtbaren Text -- nicht die Farbangabe im Stil, die weiß nichts davon, was dahinterliegt. */ const bild = await seite.screenshot({ type: "png" }); const schlimmster = await seite.evaluate(async (b64) => { const kanal = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; }; const hell = (c) => 0.2126 * kanal(c[0]) + 0.7152 * kanal(c[1]) + 0.0722 * kanal(c[2]); const kontrast = (a, b) => { const w = [hell(a), hell(b)].sort((x, y) => y - x); return (w[0] + 0.05) / (w[1] + 0.05); }; const bild = new Image(); bild.src = "data:image/png;base64," + b64; await bild.decode(); const c = document.createElement("canvas"); c.width = bild.width; c.height = bild.height; const g = c.getContext("2d"); g.drawImage(bild, 0, 0); const skala = bild.width / window.innerWidth; let schlecht = null, anzahl = 0; const alle = [...document.querySelectorAll("p, h1, h2, h3, span, a, li")] .filter((e) => e.children.length === 0 && e.textContent.trim().length > 2); for (const e of alle) { const r = e.getBoundingClientRect(); if (r.width < 8 || r.height < 8) continue; if (r.top < 0 || r.bottom > window.innerHeight) continue; const x = Math.round((r.left + r.width + 4) * skala); const y = Math.round((r.top + r.height / 2) * skala); if (x < 0 || x >= c.width || y < 0 || y >= c.height) continue; const p = g.getImageData(x, y, 1, 1).data; const st = getComputedStyle(e); const farbe = st.color.match(/\d+/g).slice(0, 3).map(Number); const gr = parseFloat(st.fontSize); const fett = Number(st.fontWeight) >= 700; const grenze = (gr >= 24 || (gr >= 18.66 && fett)) ? 3 : 4.5; const v = kontrast(farbe, [p[0], p[1], p[2]]); anzahl++; if (!schlecht || v / grenze < schlecht.v / schlecht.grenze) { schlecht = { v, grenze, text: e.textContent.trim().slice(0, 26), grund: [p[0], p[1], p[2]] }; } } return { ...schlecht, anzahl }; }, bild.toString("base64")); ok(schlimmster.v >= schlimmster.grenze, `${s}: schlechtester Kontrast ${schlimmster.v.toFixed(2)}:1 (nötig ${schlimmster.grenze}:1) ` + `bei "${schlimmster.text}" auf rgb(${schlimmster.grund.join(",")}) · ` + `${schlimmster.anzahl} Stellen gemessen`); } if (breite === 1440) { await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); await seite.waitForTimeout(800); await seite.screenshot({ path: "pruef-buehne.png" }); } await kontext.close(); } await browser.close(); console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(fehler === 0 ? 0 : 1);