Vier Wuensche auf einmal, alle auf der Startseite und der Kopfleiste. BEGRUESSUNG. Dort stand "Angemeldet" ueber dem Namen -- eine Zeile, die nichts hinzufuegt, weil oben rechts ohnehin Name und Rolle stehen. Jetzt beantwortet der Kopf die drei Fragen, die man beim Reinkommen hat: WANN (Wochentag, Datum, Kalenderwoche nach ISO 8601, selbst gerechnet), WER (Plakette mit dem Anfangsbuchstaben in der Farbe der Rolle -- der einzige runde Koerper der Seite, dadurch sofort als Person lesbar) und WIE es steht (ein Satz aus denselben Hinweisen, die darunter stehen -- keine zweite Zaehlung). An Tagen mit Charakter kommt ein Wort dazu: Neue Woche, Endspurt, Wochenende. Der Gruss ist feiner gestuft und der Name eigenstaendig ausgezeichnet: das Grusswort leise, der Name kraeftig. SCHRIFTZUG oben links. Vorher alles gleich laut, der Trenner ein Satzzeichen wie jedes andere. Jetzt hat er Rang: der Name vorn und kraeftig, der Ort dahinter und leiser, dazwischen eine kleine Raute in der Hausfarbe. Aufgeteilt zentral in kopf.js statt in sechzehn HTML- Dateien, und nur die Textknoten -- Verweise bleiben unberuehrt. HINTERGRUND heller, wie gewuenscht: Der Schleier liegt bei .86/.58/.44 statt .95/.72/.60, dazu zwei sehr weiche Farbschimmer, die dem Bild das reine Schwarz nehmen. Der Kontrast wurde danach an echten Bildpunkten nachgemessen und haelt ueberall (schlechtester Wert 4,78:1). "WISSEN" statt "Team & System" -- aber nur fuer Creator. Von der Gruppe bleibt fuer ihn genau eine Kachel uebrig; eine Ueberschrift, die Team und System verspricht und nur die Bibliothek zeigt, verspricht etwas Falsches. DREI FUNDE DURCH DIE PRUEFUNGEN: 1. Die Kontrastmessung war kaputt, seit Text neben Text steht. Sie tastete stur 4 px rechts vom Text ab -- und traf dort den NACHBARTEXT statt den Untergrund (1,10:1 gemeldet, ohne dass etwas schlecht lesbar war). Jetzt werden sechs Stellen rings um den Text angeboten und jede zuerst gefragt, ob dort wirklich Untergrund liegt. Strenger, nicht lockerer. Texte ohne freie Stelle werden GEZAEHLT und ausgegeben, nicht still uebersprungen. 2. bereich.html hatte als einzige Seite kein span.marke__text. Damit griffen dort weder die Ueberlauf-Kuerzung noch der neue Rang. Stand seit Monaten so, aufgefallen erst, als die Seitenpruefung die Marke auf allen dreizehn Seiten nachgesehen hat. 3. Die Datumspruefung haette im Maerz stillschweigend versagt: \w kennt ohne Unicode-Schalter kein "ae". Ein Fehler, der sieben Monate wartet. Co-Authored-By: Claude Opus 5 <[email protected]>
202 lines
9.5 KiB
JavaScript
202 lines
9.5 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();
|
|
|
|
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(() => ({
|
|
buehne: /buehne-(breit|schmal)/.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 kandidaten = [
|
|
[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 = 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, 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);
|