Drei handfeste Fehler, alle im Screenshot zu sehen gewesen. 1. ABGESCHNITTEN. background-size stand auf "138% auto". Auf einem 2540 px breiten Schirm wurde das Bild damit 3500 px breit und knapp 2000 px hoch -- in einem 1300 px hohen Fenster fehlten 700 px, und zwar oben. Genau deshalb waren die Figuren riesig und ihre Koepfe weg. Jetzt cover mit Verankerung auf 50% 42%: Die Flaeche wird immer gefuellt, so wenig wie noetig skaliert, und wenn etwas beschnitten werden muss, dann Fussboden und Decke -- nicht die Gesichter. 2. DIE VIGNETTE LAG FALSCH HERUM. Abgedunkelt wurde die MITTE, also genau der Teil, in dem die Szene steht. Man sah die Raender des Bildes und in der Mitte einen grauen Fleck. Jetzt laufen die RAENDER ins Dunkle und die Mitte bleibt klar -- so wie in der Fotografie. 3. ZWEI SCHICHTEN DUNKELHEIT. Zusaetzlich zur Vignette legte der Schleier .86/.58/.44 Schwarz ueber dasselbe Bild. Jetzt nur noch oben (Kopfleiste) und unten (Seitenende) ein Streifen. Dazu die Bilder selbst: Helligkeit 0.66 -> 0.88, mehr Kontrast und Farbe. Sie sind ein BILD, kein Nebel. DIE LESESPUR ist der eigentliche Kniff. In allen neun Szenen stehen HasiDog und DogFather AUSSEN, die Mitte ist frei -- danach wurden sie ausgesucht. Diese Aufteilung wird jetzt benutzt statt bekaempft: aussen bleibt das Bild hell und scharf, in der Mitte (wo der Text steht) wird gedaempft. Der klare Rand ist in jedem Fenster mindestens 400 px breit. Auf dem Handy gibt es daneben keinen Platz, dort deckt die Spur alles. Was frei stand und keine Karte hatte, bekommt eine Lesezone mit backdrop-filter: Der Hintergrund bleibt in Farbe und Form sichtbar, wird an dieser Stelle aber weichgezeichnet. Die billige Loesung waere gewesen, das Bild wieder abzudunkeln -- damit waere man dort, wo man angefangen hat. ZWEITER FUND AN DER EIGENEN KONTRASTMESSUNG. Sie tastete ausschliesslich NEBEN dem Element ab. Traegt ein Text seine Flaeche aber selbst (eine Beschriftung mit Hintergrund und Polsterung), liegt jeder Punkt daneben schon auf dem Bild -- gemeldet wurden 1,89:1, obwohl der Text auf deckender Flaeche steht und tadellos lesbar ist. Jetzt wird zuerst in der eigenen Polsterung gemessen, also auf dem, worauf der Text WIRKLICH liegt. (Der erste Fund an derselben Stelle war gestern: Sie mass Text gegen Nachbartext.) Nach dem Aufhellen einmal komplett durchgemessen: von sieben roten Stellen auf null. Schlechtester Wert jetzt 4,70:1 bei 4,5:1 Norm. Die Bilder sind groesser geworden (580 KB -> 1,8 MB fuer alle achtzehn), weil weniger Dunkelheit weniger komprimierbar ist. Geladen wird pro Seite genau eine: 42 bis 132 KB. Co-Authored-By: Claude Opus 5 <[email protected]>
232 lines
11 KiB
JavaScript
232 lines
11 KiB
JavaScript
/* ===================================================================
|
|
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();
|
|
|
|
/* Der Kalender kam am 01.09.2026 dazu -- er bringt die meisten neuen
|
|
Farbflächen mit (vier Arten, Feiertagsfärbung, Anlassmarken), und
|
|
genau dort entscheidet sich, ob der hellere Hintergrund noch trägt. */
|
|
const SEITEN = ["start.html", "uebersicht.html", "personen.html", "content.html",
|
|
"aufgaben.html", "kalender.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(() => ({
|
|
/* Seit dem 01.09.2026 gibt es neun Bühnen statt einer, benannt
|
|
nach ihrer Szene (buehne-garage-breit.webp). Der alte Ausdruck
|
|
suchte nach "buehne-breit" und fand nichts mehr -- gemeldet als
|
|
"die Bühne liegt im Hintergrund: FEHL", obwohl sie da war. */
|
|
buehne: /buehne-[a-z]+-(breit|schmal)\.webp/
|
|
.test(getComputedStyle(document.body, "::before").backgroundImage),
|
|
schleier: !!document.querySelector(".schleier"),
|
|
durchlaessig: getComputedStyle(document.querySelector(".schleier") || document.body).pointerEvents,
|
|
}));
|
|
if (s === "start.html") {
|
|
ok(da.buehne, "die Bühne 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;
|
|
|
|
/* Wo darf man den Untergrund abtasten?
|
|
|
|
Vorher wurde stur 4 px rechts neben dem Text gemessen. Das ging
|
|
gut, solange dort nichts stand -- sobald aber zwei Textstücke
|
|
nebeneinanderliegen (Grußwort und Name, Marke und Ortsangabe),
|
|
traf der Punkt DEN NACHBARTEXT und die Prüfung verglich Schrift
|
|
mit Schrift. Ergebnis: 1,1:1 und ein Alarm, der nichts mit der
|
|
Lesbarkeit zu tun hatte.
|
|
|
|
Jetzt werden mehrere Stellen rings um den Text angeboten und
|
|
jede zuerst gefragt: Liegt dort wirklich Untergrund? Ein Punkt
|
|
gilt nur, wenn an seiner Stelle KEIN fremdes Textelement liegt.
|
|
Das macht die Prüfung strenger, nicht lockerer -- sie misst
|
|
jetzt tatsächlich das, was sie behauptet zu messen. */
|
|
const istFremderText = (el2, e) =>
|
|
el2 && el2 !== e && !el2.contains(e) && !e.contains(el2)
|
|
&& el2.children.length === 0 && el2.textContent.trim().length > 0;
|
|
|
|
let schlecht = null, anzahl = 0, ohneStelle = 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 mitte = r.top + r.height / 2;
|
|
const st0 = getComputedStyle(e);
|
|
const kandidaten = [];
|
|
|
|
/* ZUERST IN DER EIGENEN POLSTERUNG.
|
|
|
|
Zweiter Fund an derselben Pruefung: Sie tastete ausschliesslich
|
|
NEBEN dem Element ab. Traegt der Text seine Flaeche aber selbst
|
|
(eine Beschriftung mit eigenem Hintergrund und Polsterung), dann
|
|
liegt jeder Punkt daneben schon auf dem Hintergrundbild -- und
|
|
gemeldet wurde 1,89:1, obwohl der Text auf einer deckenden
|
|
Flaeche steht und tadellos lesbar ist.
|
|
|
|
Der Untergrund eines Textes ist das, worauf er WIRKLICH liegt.
|
|
Hat das Element genug Polsterung, wird dort gemessen: innen,
|
|
aber sicher neben jedem Buchstaben. */
|
|
const pl = parseFloat(st0.paddingLeft) || 0;
|
|
const pt = parseFloat(st0.paddingTop) || 0;
|
|
if (pt >= 5) kandidaten.push([r.left + r.width / 2, r.top + pt / 2]);
|
|
if (pl >= 7) kandidaten.push([r.left + pl / 2, mitte]);
|
|
|
|
/* Danach wie bisher rings herum. */
|
|
kandidaten.push(
|
|
[r.right + 4, mitte], [r.right + 14, mitte],
|
|
[r.left - 4, mitte], [r.left - 14, mitte],
|
|
[r.left + r.width / 2, r.top - 3], [r.left + r.width / 2, r.bottom + 3],
|
|
);
|
|
let p = null;
|
|
for (const [kx, ky] of kandidaten) {
|
|
if (kx < 0 || ky < 0 || kx >= window.innerWidth || ky >= window.innerHeight) continue;
|
|
if (istFremderText(document.elementFromPoint(kx, ky), e)) continue;
|
|
const bx = Math.round(kx * skala), by = Math.round(ky * skala);
|
|
if (bx < 0 || bx >= c.width || by < 0 || by >= c.height) continue;
|
|
p = g.getImageData(bx, by, 1, 1).data;
|
|
break;
|
|
}
|
|
/* Kein freier Punkt gefunden -- das wird GEZÄHLT und ausgegeben.
|
|
Ein stillschweigend übersprungener Text wäre eine Prüfung, die
|
|
behauptet, etwas geprüft zu haben. */
|
|
if (!p) { ohneStelle++; continue; }
|
|
|
|
const st = st0;
|
|
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, ohneStelle, gesamt: alle.length };
|
|
}, 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`);
|
|
/* Wenn rings um einen Text nirgends freier Untergrund liegt, ist er
|
|
ungeprüft geblieben. Das darf vorkommen (dicht gesetzte Zeilen),
|
|
aber nicht unbemerkt und nicht bei vielen -- sonst meldet die
|
|
Prüfung "bestanden" für eine Seite, die sie kaum angesehen hat. */
|
|
ok(schlimmster.ohneStelle <= 2,
|
|
`${s}: höchstens zwei Texte ohne freien Untergrund `
|
|
+ `(${schlimmster.ohneStelle} von ${schlimmster.gesamt})`);
|
|
}
|
|
|
|
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);
|