Filipe: "ich will dass diese zwei kategorien perfektionniert werden, ich
will dass fertige aufgaben da stehen und so ... so dass dogfather und
rechte hand es so einfach wie möglich haben aber so professionell wie
auch nur möglich."
Bis heute waren beide Kacheln ein leeres Formular: Titel tippen, Art
wählen, Text schreiben. Ein leeres Feld beantwortet keine Frage — es
stellt sie noch einmal.
ENTWICKLUNG: 34 Beobachtungen in fünf Blöcken, vier Antworten je Zeile
(Läuft · Wächst gerade · Da hakt es · Kann ich nicht sagen). Es sind
BEOBACHTUNGEN, keine Eigenschaften — "Absagen kommen rechtzeitig" statt
"ist zuverlässig". Eine Eigenschaft kann man nur bestätigen oder
bestreiten; ein Verhalten kann man gesehen haben oder nicht.
TALENTE: 21 Merkmale in fünf Gruppen, 6 Warnzeichen getrennt geführt,
fünf Trichterstufen mit je GENAU EINEM nächsten Schritt. Eine Liste
wächst und wird nie wieder angesehen; ein Trichter fragt bei jeder Karte
"und jetzt?". Die Standzeit steht dabei — "seit 41 Tagen beobachtet"
ist eine Auskunft über uns, nicht über den Kandidaten.
VIER RIEGEL, JEDER MIT GEGENPROBE:
· Block 5 ("Wie geht es dir damit?") gehört der Person — lesend UND
schreibend, auch vor DogFather. Er kommt gar nicht erst aus dem
Server; Schreiben gibt dasselbe 404 wie ein erfundener Schlüssel.
Nach außen dringt eine Ampel OHNE Namen, und die schweigt, solange
das Team so klein ist, dass "jemandem geht es zu viel" dasselbe wäre
wie ein Name.
· Der Stand des anderen kommt erst nach dem eigenen Klick — sonst
klickt man dasselbe. Sichtbar ist nur, DASS es einen gibt: sonst
sähen "noch niemand" und "du darfst es nicht sehen" gleich aus.
· "Da hakt es" ohne Anlass wird abgelehnt. "Läuft" nicht — 28
Pflichtfelder je Person wären das Gegenteil von "so einfach wie
möglich".
· Talente gehört DogFather und der rechten Hand. Ein Modi bekommt 404
auf Seite UND Schnittstelle.
WAS NICHT GEBAUT IST, UND WARUM: keine Punktzahl, kein Durchschnitt,
keine Rangliste, keine Gesamtnote. Eine Zahl über einem Menschen wird
gelesen, verglichen und weitererzählt. Und kein Rot — "da hakt es" ist
eine Beobachtung, kein Alarm.
DER PLAN LAG AN EINER STELLE FALSCH, gemessen statt vermutet: Er sagte
"keine neue Tabelle, der Stand liegt in punkt_stand". Dort ist der
Schlüssel (bereich, schluessel, creator_id) — genau EIN Stand je Punkt
und Person. Zwei unabhängige Augenpaare passen da nicht hinein, ohne
eine lebende Tabelle neu zu bauen. Also eine eigene.
EIN RÜCKSCHLAG, DEN NUR DIE PRÜFUNG GEFUNDEN HAT: Die Liste der
Bretter, die ein Modi oder die rechte Hand öffnen darf, wird aus den
KACHELZIELEN abgeleitet. Als die Kacheln auf die neuen Seiten zeigten,
fielen beide Bretter heraus — und die Seite verlinkte auf ein 404. Kein
Fehler im Riegel, sondern in seiner Quelle.
Dazu zwei kleinere Funde vom Hinsehen statt vom Messen: Die Ampel sagte
"Solange ihr zu 1 seid" (jetzt drei Sätze für drei Lagen), und die
Fußzeile der Talente-Seite lag auf der Bühne — die Kontrastprüfung hatte
sie übersprungen, weil ein Link darin steht. Beide behoben, und die
Prüfung hat den stillen Aussetzer gleich mit verloren.
Geprüft: rollen 314 · start-ansicht 147 · crew-adresse 132 · neue-seiten
70 · nachwuchs 69 · entwicklung 40 · css-klassen 30 · zwischenspeicher
21 · alle-wege 19 · rechtetafel 19 — alles grün.
Was noch fehlt, steht in der Notiz, Abschnitt 9: die eigene Karte für
die Person, der Übergang "Probe" → Zugang, der Vorlagentext zum
Ansprechen.
Co-Authored-By: Claude Opus 5 <[email protected]>
386 lines
18 KiB
JavaScript
386 lines
18 KiB
JavaScript
/* =====================================================================
|
||
Die neuen Seiten — angesehen, nicht nur geprüft (11.09.2026)
|
||
|
||
HIESS BIS HEUTE pruef-treff-seiten.mjs. Mit den zwei Katalogseiten
|
||
(Entwicklung, Talente) stimmte der Name nicht mehr -- und ein
|
||
Dateiname, der etwas anderes verspricht als sein Inhalt, ist die
|
||
Sorte Kleinigkeit, wegen der jemand die falsche Datei oeffnet.
|
||
|
||
pruef-treff und pruef-treff-werkzeuge messen, was der Server ANTWORTET.
|
||
Diese Datei sieht nach, was auf dem Bildschirm LANDET.
|
||
|
||
Der Unterschied ist nicht theoretisch. Am 06.09.2026 hat in diesem
|
||
Haus eine Kopfleiste zwölf Seiten lang den falschen Knopf getroffen,
|
||
und keine einzige Schnittstellenprüfung hat es bemerkt — sie waren
|
||
alle grün. Gefunden hat es ein Blick.
|
||
|
||
GEPRÜFT WIRD, WAS MAN NICHT ERRATEN KANN:
|
||
|
||
* Steht überhaupt Text da? Eine Seite, deren Skript stillschweigend
|
||
abbricht, sieht in der Antwort tadellos aus und ist leer.
|
||
* Sind die Platzhalter ersetzt? Die Zahlen im Regeltext kommen vom
|
||
Server; bleibt "…" stehen, hat der Abruf nicht funktioniert.
|
||
* Liegt etwas übereinander? Zwei Bedienelemente, die sich
|
||
überlappen, sind auf dem Handy die Sorte Fehler, bei der man auf
|
||
A tippt und B bekommt.
|
||
* Lässt sich der Text lesen? Kontrast auf ECHTEN Bildpunkten
|
||
gemessen, nicht aus den Farbwerten gerechnet — die sagen nichts
|
||
über das, was hinter einer halbdurchsichtigen Fläche liegt.
|
||
* Und auf 390 px Breite: Bricht etwas aus dem Bild heraus?
|
||
|
||
Aufrufen mit: node server/pruef-neue-seiten.mjs
|
||
===================================================================== */
|
||
|
||
import { mkdtempSync, rmSync, mkdirSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
import { request as httpAnfrage } from "node:http";
|
||
import { portMussFreiSein } from "./helfer-port.mjs";
|
||
|
||
const PORT = await portMussFreiSein(4403, "pruef-neue-seiten");
|
||
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-treffs-"));
|
||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||
process.env.PORT = String(PORT);
|
||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||
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 }); };
|
||
import { notbremse } from "./helfer-notbremse.mjs";
|
||
await import("./index.js");
|
||
notbremse(240_000, "pruef-neue-seiten");
|
||
await new Promise((r) => setTimeout(r, 700));
|
||
|
||
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
||
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
||
|
||
let fehler = 0, geprueft = 0;
|
||
const melde = (t) => console.log(t);
|
||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||
|
||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||
const { DatabaseSync } = await import("node:sqlite");
|
||
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
||
|
||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
const jetzt = new Date().toISOString();
|
||
const schluessel = randomBytes(32).toString("hex");
|
||
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
||
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
||
.run("code_kennung_schluessel", schluessel, jetzt);
|
||
const ALT = new Date(Date.now() - 40 * 86400000).toISOString();
|
||
function anlegen(name, rolle, code, erstellt = jetzt) {
|
||
const salt = randomBytes(16).toString("hex");
|
||
const hash = scryptSync(code, 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, code_kennung, aktiv, erstellt)"
|
||
+ " VALUES (?,?,?,?,?,?,1,?)")
|
||
.run(name, rolle, hash, salt, 32768, createHmac("sha256", schluessel).update(code).digest("hex"), erstellt);
|
||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
}
|
||
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||
const idKessi = anlegen("Kessi", "gast", "CODE-GAST-0001", ALT);
|
||
|
||
/* ETWAS ZUM ANSEHEN. Eine leere Seite sieht aufgeräumt aus und beweist
|
||
nichts -- die Fälle auf der Moderationsseite entstehen erst, wenn es
|
||
eine Meldung und eine Maßnahme gibt. */
|
||
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von)"
|
||
+ " VALUES ('treff','hallo','Hallo, ich bin die Kessi','Freue mich, hier zu sein!',?,?,?)")
|
||
.run(jetzt.slice(0, 10), jetzt, idKessi);
|
||
const beitrag = d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
d.close();
|
||
|
||
function roh(pfad, kopf = {}, koerper = null, methode = null) {
|
||
if (methode === "DELETE") koerper = null;
|
||
return new Promise((fertig, schief) => {
|
||
const a = httpAnfrage({
|
||
host: "127.0.0.1", port: PORT, path: pfad,
|
||
method: methode || (koerper ? "POST" : "GET"),
|
||
headers: { ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf },
|
||
}, (antwort) => {
|
||
let text = "";
|
||
antwort.on("data", (s) => { text += s; });
|
||
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
|
||
});
|
||
a.on("error", schief);
|
||
if (koerper) a.write(JSON.stringify(koerper));
|
||
a.end();
|
||
});
|
||
}
|
||
|
||
/* Eine Meldung und eine Maßnahme anlegen -- über die echten Wege. */
|
||
const anm = await roh("/workspace/api/anmelden", {}, { rolle: "gast", code: "CODE-GAST-0001" });
|
||
const kGast = [].concat(anm.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
|
||
const anm2 = await roh("/workspace/api/anmelden", {}, { rolle: "admin", code: "CODE-DOGI-0001" });
|
||
const kDogi = [].concat(anm2.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
|
||
await roh(`/workspace/api/treff/eintrag/${beitrag}/melden`, { cookie: kGast },
|
||
{ grund: "Da steht eine Handynummer drin, die da nicht hingehört." });
|
||
await roh("/workspace/api/treff/massnahme", { cookie: kDogi },
|
||
{ person_id: idKessi, art: "hinweis", grund: "Bitte keine Nummern posten – einmalig, kein Drama." });
|
||
|
||
/* ETWAS AUF DEN KATALOGSEITEN. Ohne eine Person im Team ist die
|
||
Entwicklungsseite leer, ohne einen Kandidaten der Trichter -- und
|
||
eine leere Seite erfuellt jede Bedingung, ohne etwas zu zeigen. */
|
||
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
{
|
||
const salt = randomBytes(16).toString("hex");
|
||
const hash = scryptSync("CODE-MODI-0001", salt, 64,
|
||
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||
dd.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
||
+ " VALUES ('Frida','modi',?,?,?,?,1,?)")
|
||
.run(hash, salt, 32768,
|
||
createHmac("sha256", schluessel).update("CODE-MODI-0001").digest("hex"), jetzt);
|
||
dd.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)"
|
||
+ " VALUES ('talente','ruhig','Kessi aus dem Chat','Faellt seit Wochen auf.',?,?,?,?)")
|
||
.run(jetzt.slice(0, 10), jetzt, idDogi, "Im Stream am Dienstag");
|
||
}
|
||
dd.close();
|
||
|
||
const { chromium } = await import(
|
||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||
const browser = await chromium.launch();
|
||
|
||
const BILDER = join(ordner, "bilder");
|
||
mkdirSync(BILDER, { recursive: true });
|
||
|
||
/* Kontrast auf ECHTEN Bildpunkten: Die Schrift liegt teils über
|
||
halbdurchsichtigen Flächen und über der Bühne; aus den Farbwerten im
|
||
CSS lässt sich der tatsächliche Kontrast deshalb nicht ausrechnen. */
|
||
const KONTRAST = `(() => {
|
||
const leuchte = (r, g, b) => {
|
||
const f = (c) => { c /= 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); };
|
||
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
||
};
|
||
const misch = (v, h) => v.map((x, i) => x * v[3] + h[i] * (1 - v[3]));
|
||
const grund = (e) => {
|
||
let n = e;
|
||
while (n && n !== document.documentElement) {
|
||
const f = getComputedStyle(n).backgroundColor;
|
||
const m = f.match(/rgba?\\(([^)]+)\\)/);
|
||
if (m) {
|
||
const t = m[1].split(",").map(Number);
|
||
const a = t.length > 3 ? t[3] : 1;
|
||
if (a > 0.92) return [t[0], t[1], t[2], 1];
|
||
}
|
||
n = n.parentElement;
|
||
}
|
||
return null;
|
||
};
|
||
const schlecht = [];
|
||
const geraten = [];
|
||
let gemessen = 0;
|
||
for (const e of document.querySelectorAll("p, li, h1, h2, h3, td, th, span, a, button")) {
|
||
const t = (e.textContent || "").trim();
|
||
if (!t) continue;
|
||
/* ELEMENTE MIT KINDERN WERDEN UEBERSPRUNGEN, und das ist richtig:
|
||
Ihr textContent ist der Text der Kinder, die einzeln drankommen.
|
||
Es war aber ein STILLER Aussetzer -- ein <p> mit einem Link
|
||
darin fiel heraus, und der eigene Text daneben wurde nie
|
||
gemessen. Genau so ist die Fusszeile der Talente-Seite durch die
|
||
Pruefung gekommen, waehrend sie auf der Buehne lag.
|
||
|
||
Jetzt zaehlt der EIGENE Text: Hat ein Element neben seinen
|
||
Kindern eigene Zeichen, wird es mitgemessen. */
|
||
const eigen = [...e.childNodes]
|
||
.filter((n) => n.nodeType === 3).map((n) => n.textContent.trim()).join("");
|
||
if (e.children.length && eigen.length < 3) continue;
|
||
const k = e.getBoundingClientRect();
|
||
if (k.width < 4 || k.height < 4) continue;
|
||
const s = getComputedStyle(e);
|
||
if (s.visibility === "hidden" || s.display === "none" || Number(s.opacity) < 0.2) continue;
|
||
const m = s.color.match(/rgba?\\(([^)]+)\\)/);
|
||
if (!m) continue;
|
||
const v = m[1].split(",").map(Number);
|
||
/* DRITTER AUSGANG: konnte nicht nachsehen.
|
||
|
||
Findet sich über diesem Text keine deckende Fläche, dann liegt er
|
||
auf der Bühne — einem Foto. Was dahinter liegt, lässt sich aus
|
||
den Farbwerten NICHT ausrechnen, und eine angenommene
|
||
Hintergrundfarbe wäre genau der grüne Haken über einer falschen
|
||
Voraussetzung. Genau so ist am 11.09.2026 die Rechtetafel durch
|
||
die Messung gekommen, während im Bildschirmfoto Chilischoten
|
||
hinter den Zeilen standen. */
|
||
const hinten = grund(e);
|
||
if (!hinten) { geraten.push(t.slice(0, 34)); continue; }
|
||
const vorn = misch([v[0], v[1], v[2], v.length > 3 ? v[3] : 1], hinten);
|
||
const l1 = leuchte(...vorn), l2 = leuchte(...hinten);
|
||
const k2 = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
||
gemessen++;
|
||
const gross = parseFloat(s.fontSize) >= 24
|
||
|| (parseFloat(s.fontSize) >= 18.66 && Number(s.fontWeight) >= 700);
|
||
if (k2 < (gross ? 3 : 4.5)) schlecht.push(t.slice(0, 34) + " (" + k2.toFixed(2) + ")");
|
||
}
|
||
return { gemessen, schlecht, geraten };
|
||
})()`;
|
||
|
||
/* WAS ABSICHTLICH ÜBEREINANDERLIEGT, IST KEIN FEHLER (11.09.2026).
|
||
|
||
Der erste Entwurf meldete auf allen drei Seiten dasselbe:
|
||
"Meine SichtKessi / Meine Sicht". Das ist der Umschalter in der
|
||
Kopfleiste — ein selbst gebautes Bedienelement, unter dem ein echtes
|
||
<select> für Vorleseprogramme liegt. Es MUSS dort liegen, sonst
|
||
findet es niemand mit einem Bildschirmleser.
|
||
|
||
Es ist für das Auge unsichtbar gemacht, aber nicht über
|
||
`visibility` oder `opacity` — sonst würden manche Vorleseprogramme
|
||
es ebenfalls überspringen. Übliche Technik ist ein 1x1-Ausschnitt
|
||
(`clip-path`) oder eine Verschiebung aus dem Bild.
|
||
|
||
Eine Prüfung, die so etwas meldet, ist schlimmer als keine: Sie ist
|
||
auf JEDER Seite rot, man gewöhnt sich daran, und die eine echte
|
||
Überlappung geht darin unter. Ausgelassen wird deshalb, was
|
||
ausdrücklich für das Auge weggenommen wurde — und NICHT einfach
|
||
alles, was "Meine Sicht" heißt. */
|
||
const UEBERLAPP = `(() => {
|
||
const versteckt = (e) => {
|
||
const s = getComputedStyle(e);
|
||
if (s.clipPath && s.clipPath !== "none") return true;
|
||
if (s.clip && s.clip !== "auto") return true;
|
||
if (Number(s.opacity) < 0.2) return true;
|
||
if (e.getAttribute("aria-hidden") === "true") return true;
|
||
const k = e.getBoundingClientRect();
|
||
return k.right < 1 || k.bottom < 1;
|
||
};
|
||
const teile = [...document.querySelectorAll("button, a.schritt, input, select")]
|
||
.filter((e) => { const k = e.getBoundingClientRect(); return k.width > 4 && k.height > 4; })
|
||
.filter((e) => !versteckt(e));
|
||
const treffer = [];
|
||
for (let i = 0; i < teile.length; i++) {
|
||
for (let j = i + 1; j < teile.length; j++) {
|
||
if (teile[i].contains(teile[j]) || teile[j].contains(teile[i])) continue;
|
||
const a = teile[i].getBoundingClientRect(), b = teile[j].getBoundingClientRect();
|
||
const x = Math.min(a.right, b.right) - Math.max(a.left, b.left);
|
||
const y = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
|
||
if (x > 2 && y > 2) {
|
||
treffer.push((teile[i].textContent || "?").trim().slice(0, 18)
|
||
+ " / " + (teile[j].textContent || "?").trim().slice(0, 18));
|
||
}
|
||
}
|
||
}
|
||
return treffer;
|
||
})()`;
|
||
|
||
/* ÜBERBREITE: DIE FRAGE IST, OB DIE SEITE SCHIEBT (11.09.2026).
|
||
|
||
Der erste Entwurf zählte jedes Element, dessen rechte Kante über den
|
||
Rand ragt. Er meldete die Rechtetafel auf 390 px — zu Recht in dem
|
||
Sinne, dass die Tabelle breiter ist als das Bild, und zu Unrecht in
|
||
jedem anderen: Sie steht in einem Kasten, den man seitwärts schieben
|
||
kann (`overflow-x: auto`), und genau so soll eine Tabelle mit neun
|
||
Spalten sich auf einem Handy verhalten. Alles andere hieße, Spalten
|
||
wegzulassen oder Schrift unlesbar zu verkleinern.
|
||
|
||
Schlimm ist nur, wenn die GANZE SEITE seitwärts schiebt — dann
|
||
verrutscht beim Lesen der Text unter dem Daumen. Das ist eine
|
||
einzige Zahl, und sie lügt nicht: scrollWidth gegen clientWidth am
|
||
<html>. Was in einem Schiebekasten steht, zählt dort nicht mit.
|
||
|
||
Die Liste der Ausreißer bleibt trotzdem — aber nur als Auskunft
|
||
DANEBEN, und Elemente in einem Schiebekasten sind ausgenommen. */
|
||
const UEBERBREIT = `(() => {
|
||
const wurzel = document.documentElement;
|
||
const schiebt = wurzel.scrollWidth - wurzel.clientWidth;
|
||
const imKasten = (e) => {
|
||
let n = e.parentElement;
|
||
while (n && n !== document.body) {
|
||
const s = getComputedStyle(n);
|
||
if (s.overflowX === "auto" || s.overflowX === "scroll") return true;
|
||
n = n.parentElement;
|
||
}
|
||
return false;
|
||
};
|
||
const w = wurzel.clientWidth;
|
||
const raus = [...document.querySelectorAll("main *")]
|
||
.filter((e) => { const k = e.getBoundingClientRect(); return k.right > w + 2 && k.width > 8; })
|
||
.filter((e) => !imKasten(e))
|
||
.map((e) => e.tagName.toLowerCase() + "." + (e.className || "").toString().split(" ")[0])
|
||
.slice(0, 5);
|
||
return { schiebt, raus };
|
||
})()`;
|
||
|
||
async function ansehen(name, pfad, code, breite, hoehe) {
|
||
const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } });
|
||
const seite = await kontext.newPage();
|
||
const konsole = [];
|
||
seite.on("console", (m) => {
|
||
if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text());
|
||
});
|
||
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
||
|
||
await seite.goto(`${BASIS}/workspace/`, { waitUntil: "domcontentloaded" });
|
||
await seite.click(`.rolle[data-rolle="admin"]`);
|
||
await seite.fill("#code", code);
|
||
await seite.click("button[type=submit]");
|
||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||
|
||
await seite.goto(`${BASIS}/workspace/${pfad}`, { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(900);
|
||
|
||
const text = await seite.evaluate(() => (document.querySelector("main")?.innerText || "").trim());
|
||
const punkte = await seite.evaluate(() => (document.querySelector("main")?.innerText || "")
|
||
.split("\\n").filter((z) => z.trim() === "…").length);
|
||
const kontrast = await seite.evaluate(KONTRAST);
|
||
const ueberlapp = await seite.evaluate(UEBERLAPP);
|
||
const breit = await seite.evaluate(UEBERBREIT);
|
||
await seite.screenshot({ path: join(BILDER, `${name}-${breite}.png`), fullPage: true });
|
||
await kontext.close();
|
||
return { text, punkte, kontrast, ueberlapp, breit, konsole };
|
||
}
|
||
|
||
for (const [name, pfad] of [
|
||
["regeln", "treff-regeln.html"],
|
||
["rechte", "rechte.html"],
|
||
["moderation", "treff-moderation.html"],
|
||
/* Die zwei Katalogseiten. Sie stehen hier und nicht in
|
||
pruef-nachwuchs: Dort wird gemessen, was der Server ANTWORTET,
|
||
hier, was auf dem Bildschirm LANDET. Beides ist noetig, und beides
|
||
findet andere Fehler. */
|
||
["entwicklung", "entwicklung.html"],
|
||
["talente", "talente.html"],
|
||
]) {
|
||
melde("");
|
||
melde(`=== ${pfad} ===`);
|
||
for (const [breite, hoehe] of [[1440, 1000], [390, 844]]) {
|
||
const r = await ansehen(name, pfad, "CODE-DOGI-0001", breite, hoehe);
|
||
const wo = `${breite} px`;
|
||
|
||
ok(r.text.length > 400, `${wo}: die Seite ist gefüllt (${r.text.length} Zeichen)`);
|
||
/* DER PLATZHALTER IST DER EIGENTLICHE PUNKT. Bleibt "…" stehen, hat
|
||
der Abruf der Zahlen nicht funktioniert -- und der Regeltext
|
||
verspricht dann gar nichts statt etwas Falsches. Sichtbar wäre
|
||
das nur hier. */
|
||
ok(r.punkte === 0, `${wo}: keine Platzhalter mehr übrig (${r.punkte})`);
|
||
ok(r.konsole.length === 0,
|
||
`${wo}: keine Fehler im Browser${r.konsole.length ? " -- " + r.konsole[0].slice(0, 80) : ""}`);
|
||
ok(r.ueberlapp.length === 0,
|
||
`${wo}: nichts liegt übereinander${r.ueberlapp.length ? " -- " + r.ueberlapp.join(" | ") : ""}`);
|
||
ok(r.breit.schiebt <= 1 && r.breit.raus.length === 0,
|
||
`${wo}: die Seite schiebt nicht seitwärts (${r.breit.schiebt} px)`
|
||
+ (r.breit.raus.length ? " -- DRAUSSEN: " + r.breit.raus.join(", ") : ""));
|
||
/* DIE ZAHL GEHÖRT IN DIE BEDINGUNG: Fände die Messung kein einziges
|
||
Textstück, wäre "nichts zu dunkel" trivial wahr. */
|
||
ok(r.kontrast.gemessen > 15 && r.kontrast.schlecht.length === 0,
|
||
`${wo}: ${r.kontrast.gemessen} Textstücke, alle lesbar`
|
||
+ (r.kontrast.schlecht.length ? " -- ZU DUNKEL: " + r.kontrast.schlecht.slice(0, 3).join(" | ") : ""));
|
||
/* Und die Frage davor: Konnte überhaupt gemessen werden? Text, der
|
||
auf der Bühne liegt, hat keinen berechenbaren Kontrast -- die
|
||
Zeile darüber wäre für ihn grün geblieben, ohne ihn anzusehen. */
|
||
ok(r.kontrast.geraten.length === 0,
|
||
`${wo}: jeder Text steht auf einer deckenden Fläche`
|
||
+ (r.kontrast.geraten.length
|
||
? ` -- AUF DER BÜHNE (${r.kontrast.geraten.length}): `
|
||
+ r.kontrast.geraten.slice(0, 3).join(" | ") : ""));
|
||
}
|
||
}
|
||
|
||
await browser.close();
|
||
melde("");
|
||
melde(`Bilder liegen in ${BILDER}`);
|
||
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
||
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
||
/* Der Ordner bleibt liegen, solange der Dienst die Datenbank offen hat
|
||
-- Aufräumrest, kein Befund. Die Bilder darin sind ohnehin das
|
||
Ergebnis; sie werden bewusst NICHT mit weggeräumt. */
|
||
process.exit(fehler ? 1 : 0);
|