Weiter am Rundumcheck. Diese Runde hat KEINE Zeile am Programm
geaendert: Alle drei Befunde kamen aus den Pruefungen selbst. Das ist
kein Trost, sondern die unangenehmere Sorte -- ein Fehlalarm nennt eine
echte Stelle und eine plausible Zahl, und man sucht danach am richtigen
Code.
1. EIN FARBFORMAT, DAS DIE MESSUNG NICHT KANNTE.
`pruef-neue-seiten` meldete auf treff-regeln.html einen Kontrast von
1,07 -- praktisch unsichtbarer Text. Nachgemessen an Ort und Stelle:
dunkler Text auf HELLBLAUER Flaeche, rund 10:1.
Chromium gibt eine Farbe, die aus `color-mix()` entstanden ist, als
`color(srgb 0.37 0.78 0.97)` zurueck -- Werte von 0 bis 1, keine
Kommas. Die Messung las nur `rgba?(...)`, fand die Flaeche des
Sprunglinks nicht, ging zum fast schwarzen Elternteil weiter und
rechnete dunkel gegen dunkel. Im Haus ist `color-mix` ueberall.
Der Farbleser kennt jetzt beide Formate und faehrt vier Proben mit
(rgb, rgba, color(srgb), color(srgb / alpha)) -- dazu eine fuenfte,
die beweist, dass er bei Unbekanntem NICHTS liefert. Ein Leser, der
im Zweifel Schwarz zurueckgibt, waere schlimmer als der alte: Er
wuerde nie wieder auffallen.
2. EINE SEITE, DIE ZU KURZ WAR, UM "GEFUELLT" ZU HEISSEN.
Dieselbe Pruefung verlangte von jeder Seite mehr als 400 Zeichen
Text. entwicklung.html hat 350 -- seit dem Umbau, bei dem aus
achtzehn Zahlenkaesten zwei Personenkacheln mit einem Ring wurden.
Die Seite sagt seither mehr und braucht weniger Worte; die Pruefung
bestrafte damit genau die Verbesserung.
Jede Seite darf jetzt sagen, woran man sieht, dass sie da ist: eine
Zeichenzahl, wo Text die Sache ist -- ein MERKMAL (".e-person",
mindestens zwei), wo Struktur die Sache ist. Eine Zahl kann beides
nicht unterscheiden.
3. EIN ABSCHNITT, DER SPAETER KAM ALS DIE MESSUNG.
"Die rechte Hand sieht ihre eigene Karte" war rot -- versteckt,
obwohl der Server `hat_karte: true` sagte. Von Hand nachgesehen
stand sie da. Die Entwicklungsseite laedt nacheinander (Aufgaben,
Vorlagenbrett, eigene Karte), und zwischen zwei Schritten kann es
laenger als 600 ms still sein -- die Ruheregel der Pruefung hielt
das fuer "fertig". Jetzt wartet sie zusaetzlich auf den einen
Abschnitt, der sich selbst aufdeckt, hoechstens zwei Sekunden.
Nebenwirkung: Der Vergleich "zwei verschiedene Bildschirme" misst
jetzt 632 gegen 1191 Zeichen statt 350 gegen 350.
109 Pruefungen, 0 Fehler (vorher 8).
4. UND EINE FESTE ZAHL, DIE MIT DER SEITE GEWACHSEN IST.
`pruef-buehne` erlaubte hoechstens ZWEI Texte, um die herum kein
freier Untergrund zu finden ist. treff-regeln.html hat inzwischen
40 Textstuecke, drei davon stehen dicht -- 7,5 %. Die Sorge dahinter
bleibt richtig (eine Pruefung, die kaum hingesehen hat, darf nicht
"bestanden" sagen), aber das ist eine Frage des ANTEILS: zwei von
vier waere schlimm, drei von vierzig ist es nicht. Jetzt: zwei
immer erlaubt, darueber ein Zehntel -- und die Meldung nennt die
Textanfaenge, damit man in einer Sekunde sieht, worum es geht.
ZWEI NEUE WERKZEUGE, beide aus der Not dieser Runde entstanden:
mess-farbe-am-text.mjs -- liest die Farben an Ort und Stelle aus und
geht die Elternkette hoch bis zur ersten
deckenden Flaeche
mess-seiteninhalt.mjs -- zeigt, was auf einer Seite wirklich steht,
mit ROLLE=hand auch mit fremden Augen
GEMESSEN: pruef-neue-seiten 109/0, pruef-buehne alles in Ordnung.
Co-Authored-By: Claude Opus 5 <[email protected]>
728 lines
36 KiB
JavaScript
728 lines
36 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 { eigenerPort } from "./helfer-port.mjs";
|
||
|
||
const PORT = await eigenerPort(import.meta, "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. */
|
||
/* alter_ok: Die Tuer fragt die Community seit dem 15.09.2026 beim
|
||
ersten Hereinkommen nach dem Alter. */
|
||
const anm = await roh("/workspace/api/anmelden", {},
|
||
{ rolle: "gast", code: "CODE-GAST-0001", alter_ok: true });
|
||
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.
|
||
|
||
UND ZWAR AUF MEHREREN STUFEN. Der Trichter zeigt je Stufe einen
|
||
ANDEREN naechsten Schritt; steht nur ein Kandidat auf der ersten,
|
||
bleiben die Knoepfe der anderen vier ungesehen. Genau dort sitzen
|
||
seit dem 11.09. die zwei neuen: der Vorlagentext auf "Beobachtet"
|
||
und "Ins Team holen · Zugang anlegen" auf "Probe". */
|
||
/* EINMAL ANKLOPFEN, DAMIT ES DIE TABELLEN GIBT. Sie entstehen beim
|
||
ersten angemeldeten Aufruf des Bereichs, nicht beim Start -- ein
|
||
INSERT davor liefe gegen eine Tabelle, die es noch nicht gibt. */
|
||
await roh("/workspace/api/talente/lage", { cookie: kDogi });
|
||
const kat = await import("./workspace-entwicklungs-punkte.js");
|
||
const talKat = await import("./workspace-talent-punkte.js");
|
||
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
let idHand = null;
|
||
{
|
||
const person = (name, rolle, code) => {
|
||
const salt = randomBytes(16).toString("hex");
|
||
const hash = scryptSync(code, 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 (?,?,?,?,?,?,1,?)")
|
||
.run(name, rolle, hash, salt, 32768,
|
||
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
|
||
return dd.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
};
|
||
person("Frida", "modi", "CODE-MODI-0001");
|
||
idHand = person("Rieke", "hand", "CODE-HAND-0001");
|
||
|
||
const talent = (titel, text) => {
|
||
dd.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, erstellt, erstellt_von, einsatz)"
|
||
+ " VALUES ('talente','ruhig',?,?,?,?,?,?)")
|
||
.run(titel, text, jetzt.slice(0, 10), jetzt, idDogi, "Im Stream am Dienstag");
|
||
return dd.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
};
|
||
talent("Kessi aus dem Chat", "Faellt seit Wochen auf.");
|
||
const idBeobachtet = talent("Malin aus dem Chat", "Haelt den Chat ruhig.");
|
||
const idProbe = talent("Tino aus dem Chat", "Laeuft seit vier Wochen mit.");
|
||
|
||
const stufe = dd.prepare(
|
||
"INSERT INTO talent_stufe (eintrag_id, stufe, seit, von_id) VALUES (?,?,?,?)");
|
||
stufe.run(idBeobachtet, "beobachtet", jetzt, idDogi);
|
||
stufe.run(idProbe, "probe", jetzt, idDogi);
|
||
|
||
/* MERKMALE fuer den Vorlagentext -- er wird daraus gebaut. Ohne sie
|
||
stuende dort der Satz fuer "noch nichts beobachtet", und die neue
|
||
Gestaltung waere zwar sichtbar, aber nicht in ihrer laengsten
|
||
Fassung. Gemessen werden soll die laengste. */
|
||
const merk = dd.prepare(
|
||
"INSERT INTO talent_merkmal (eintrag_id, schluessel, von_id, am) VALUES (?,?,?,?)");
|
||
for (const m of talKat.TALENT_ALLE.slice(0, 3)) {
|
||
merk.run(idBeobachtet, m.schluessel, idDogi, jetzt);
|
||
}
|
||
|
||
/* DIE EIGENE KARTE DER RECHTEN HAND. Sie oeffnet sich erst, wenn
|
||
jeder Beurteiler gesetzt hat -- ueber sie urteilt nur DogFather,
|
||
also reicht eine Zeile je Punkt. Sechs gesetzte und der Rest offen:
|
||
So stehen beide Zustaende nebeneinander auf dem Bildschirm. */
|
||
const stand = dd.prepare(
|
||
"INSERT INTO entwicklung_stand (person_id, schluessel, von_id, stufe, anlass, am)"
|
||
+ " VALUES (?,?,?,?,?,?)");
|
||
const stufen = ["laeuft", "waechst", "laeuft", "hakt", "laeuft", "waechst"];
|
||
kat.ENTWICKLUNG_FREMD.slice(0, 6).forEach((p, i) => {
|
||
stand.run(idHand, p.schluessel, idDogi, stufen[i],
|
||
stufen[i] === "hakt" ? "Zweimal zu spaet zur Schicht." : null, jetzt);
|
||
});
|
||
}
|
||
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]));
|
||
/* ==== ZWEI FARBFORMATE, NICHT EINS (25.09.2026) ==================
|
||
|
||
Hier stand nur \`rgba?(...)\`. Chromium gibt eine Farbe, die aus
|
||
\`color-mix()\` entstanden ist, aber als \`color(srgb 0.37 0.78
|
||
0.97)\` zurueck -- mit Werten von 0 bis 1 statt 0 bis 255 und
|
||
ohne Kommas. Im Haus ist \`color-mix\` ueberall im Einsatz.
|
||
|
||
WAS DAS ANGERICHTET HAT: Der Sprunglink auf treff-regeln.html
|
||
traegt eine hellblaue Flaeche und dunklen Text -- ein Kontrast von
|
||
rund 10:1. Die Messung konnte seine Flaeche nicht lesen, ging zum
|
||
Elternteil weiter (fast schwarz) und rechnete dunklen Text gegen
|
||
dunklen Grund: 1,07. Ein Befund, der wie ein blinder Text aussah
|
||
und keiner war.
|
||
|
||
Das ist die unangenehmste Sorte Fehlalarm: Er zeigt auf eine
|
||
echte Stelle, nennt eine plausible Zahl -- und schickt einen in
|
||
ein CSS, an dem nichts ist. Gefunden habe ich es, indem ich die
|
||
Farben an Ort und Stelle ausgelesen habe, statt der Meldung zu
|
||
glauben.
|
||
|
||
DIE GEGENPROBE STEHT UNTEN: Ein erfundenes \`color(srgb ...)\`
|
||
muss als Farbe erkannt werden, und ein unbekanntes Format muss
|
||
weiterhin NICHTS liefern -- sonst faengt die Messung an, Werte zu
|
||
erfinden. */
|
||
const farbe = (f) => {
|
||
const m = f.match(/rgba?\\(([^)]+)\\)/);
|
||
if (m) {
|
||
const t = m[1].split(",").map(Number);
|
||
return [t[0], t[1], t[2], t.length > 3 ? t[3] : 1];
|
||
}
|
||
/* color(srgb r g b / a) -- Anteile von 0 bis 1, durch Leerzeichen
|
||
getrennt, das Alpha hinter einem Schraegstrich. */
|
||
const c = f.match(/color\\(\\s*srgb\\s+([^)]+)\\)/);
|
||
if (c) {
|
||
const roh = c[1].split("/");
|
||
const t = roh[0].trim().split(/\\s+/).map(Number);
|
||
const a = roh.length > 1 ? Number(roh[1]) : 1;
|
||
if (t.length >= 3 && t.every((x) => Number.isFinite(x))) {
|
||
return [t[0] * 255, t[1] * 255, t[2] * 255, Number.isFinite(a) ? a : 1];
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
const grund = (e) => {
|
||
let n = e;
|
||
while (n && n !== document.documentElement) {
|
||
const t = farbe(getComputedStyle(n).backgroundColor);
|
||
if (t && t[3] > 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 v = farbe(s.color);
|
||
if (!v) continue;
|
||
/* 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, 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) + ")");
|
||
}
|
||
/* GEGENPROBE IM MESSPUNKT SELBST -- sie kostet nichts und beweist,
|
||
dass der Leser beide Formate kann und nicht raet. */
|
||
const probe = {
|
||
rgb: farbe("rgb(10, 18, 30)"),
|
||
rgba: farbe("rgba(255, 255, 255, 0.5)"),
|
||
srgb: farbe("color(srgb 0.367529 0.782588 0.967059)"),
|
||
srgbAlpha: farbe("color(srgb 1 0 0 / 0.25)"),
|
||
unsinn: farbe("wasauchimmer"),
|
||
};
|
||
return { gemessen, schlecht, geraten, probe };
|
||
})()`;
|
||
|
||
/* 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 };
|
||
})()`;
|
||
|
||
/* MIT EINEM FERTIGEN KEKS STATT ueber die Anmeldemaske (11.09.2026).
|
||
|
||
Die rechte Hand meldet sich an der CREW-WAND an, und die haengt am
|
||
Host-Kopf -- den kann ein Browser bei einer Navigation nicht setzen.
|
||
Ueber die Maske auf 127.0.0.1 kommt sie also gar nicht herein.
|
||
|
||
Das ist kein Umgehen einer Sicherung: Angemeldet wird ueber denselben
|
||
Weg wie sonst (/api/anmelden), nur die Maske bleibt aussen vor. Was
|
||
die Seite danach zeigt, entscheidet weiterhin der Server. */
|
||
async function ansehen(name, pfad, code, breite, hoehe, keks = null, merkmal = null) {
|
||
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));
|
||
|
||
if (keks) {
|
||
await kontext.addCookies(keks.split("; ").filter(Boolean).map((teil) => {
|
||
const i = teil.indexOf("=");
|
||
return { name: teil.slice(0, i), value: teil.slice(i + 1), domain: "127.0.0.1", path: "/" };
|
||
}));
|
||
} else {
|
||
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" });
|
||
/* WARTEN, BIS SICH NICHTS MEHR AENDERT -- NICHT 900 MILLISEKUNDEN.
|
||
|
||
Gemessen am 11.09.2026: Die eigene Karte der rechten Hand stand
|
||
nachweislich auf dem Bildschirm (versteckt: false, Antwort 200),
|
||
aber der Text war schon vorher gelesen. Die Pruefung meldete
|
||
"steht nicht da" fuer etwas, das dastand -- der schlimmste
|
||
Fehlalarm, weil man danach am richtigen Code sucht.
|
||
|
||
Eine groessere Zahl waere derselbe Fehler mit einer anderen Zahl.
|
||
Die Seite laedt ihre Abschnitte nacheinander; wie lange das
|
||
dauert, haengt an ihrer Anzahl. Deshalb eine Regel statt einer
|
||
Rechnung: gelesen wird, wenn der Text dreimal hintereinander
|
||
derselbe war. Die Frist darueber ist nur die Notbremse. */
|
||
await seite.waitForFunction(() => {
|
||
const t = document.querySelector("main")?.innerText || "";
|
||
if (window.__standStill === t) { window.__ruhig = (window.__ruhig || 0) + 1; }
|
||
else { window.__standStill = t; window.__ruhig = 0; }
|
||
return (window.__ruhig || 0) >= 3;
|
||
}, null, { timeout: 12000, polling: 200 }).catch(() => {});
|
||
|
||
/* ==== UND NOCH AUF DEN LETZTEN ABSCHNITT (25.09.2026) ===========
|
||
|
||
Die Ruheregel darueber ist richtig und reicht hier nicht: Die
|
||
Entwicklungsseite laedt NACHEINANDER -- Aufgaben, Vorlagenbrett,
|
||
eigene Karte. Zwischen zwei dieser Schritte kann es laenger als
|
||
600 ms still sein, und dann gilt die Seite als fertig, waehrend
|
||
der letzte Abschnitt noch unterwegs ist.
|
||
|
||
Gemeldet wurde dadurch: „die rechte Hand sieht ihre eigene Karte"
|
||
-- versteckt, obwohl der Server `hat_karte: true` sagte. Von Hand
|
||
nachgesehen steht sie da („DEINE KARTE / Noch nichts fertig ...").
|
||
Also wieder die Sorte Fehlalarm, die einen am richtigen Code
|
||
suchen laesst.
|
||
|
||
GEWARTET WIRD NUR, WENN ES ETWAS ZU WARTEN GIBT: Seiten ohne
|
||
`#karte-block` laufen sofort weiter; DogFather hat keine Karte,
|
||
dort bleibt der Block zu Recht zu, und die Frist von zwei
|
||
Sekunden ist die Notbremse, kein Taktgeber. */
|
||
await seite.waitForFunction(() => {
|
||
const e = document.getElementById("karte-block");
|
||
return !e || !e.hidden;
|
||
}, null, { timeout: 2000, polling: 100 }).catch(() => {});
|
||
|
||
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);
|
||
/* DIE EIGENE KARTE IST DER EINZIGE ABSCHNITT, DER SICH SELBST
|
||
AUFDECKT. Bleibt er zu, sieht die Seite vollkommen normal aus --
|
||
deshalb wird hier nicht nur gelesen, was dasteht, sondern gefragt,
|
||
was der Server dazu sagt. */
|
||
const karte = await seite.evaluate(async () => {
|
||
const e = document.getElementById("karte-block");
|
||
if (!e) return { da: false };
|
||
let antwort = null;
|
||
try {
|
||
const a = await fetch("/workspace/api/entwicklung/meine-karte", { credentials: "same-origin" });
|
||
antwort = { status: a.status, hat: (await a.json()).hat_karte };
|
||
} catch (f) { antwort = { status: -1, fehler: String(f) }; }
|
||
return { da: true, versteckt: e.hidden, antwort };
|
||
});
|
||
/* Wie oft steht das Merkmal auf der Seite, das sie ausmacht? Null,
|
||
wenn keins genannt ist -- dann zaehlt die Zeichenzahl.
|
||
|
||
VOR dem Schliessen. Im ersten Anlauf stand diese Zeile darunter,
|
||
und die Meldung lautete „Target page, context or browser has been
|
||
closed" -- eine Frage an eine Seite, die es nicht mehr gibt. */
|
||
const merkmale = merkmal
|
||
? await seite.evaluate((m) => document.querySelectorAll(m).length, merkmal)
|
||
: 0;
|
||
|
||
await seite.screenshot({ path: join(BILDER, `${name}-${breite}.png`), fullPage: true });
|
||
await kontext.close();
|
||
|
||
return { text, punkte, kontrast, ueberlapp, breit, konsole, karte, merkmale };
|
||
}
|
||
|
||
const gesehen = {}; // was auf jedem Bildschirm tatsaechlich stand
|
||
|
||
const anm3 = await roh("/workspace/api/anmelden", {}, { rolle: "hand", code: "CODE-HAND-0001" });
|
||
const kHand = [].concat(anm3.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
|
||
ok(!!kHand, "die rechte Hand ist angemeldet -- ohne sie bliebe ihre eigene Karte ungesehen");
|
||
|
||
/* ==== "GEFUELLT" IST KEINE ZEICHENZAHL (25.09.2026) =================
|
||
|
||
Hier wurde fuer jede Seite `text.length > 400` verlangt. Das ist
|
||
eine sinnvolle Faustregel fuer eine TEXTSEITE -- und eine Falle fuer
|
||
jede andere.
|
||
|
||
entwicklung.html ist am 24./25.09. umgebaut worden: Aus achtzehn
|
||
Zahlenkaesten wurden zwei Personenkacheln mit einem Ring. Die Seite
|
||
sagt seither MEHR und braucht dafuer WENIGER Worte -- 350 Zeichen.
|
||
Die Pruefung wurde rot und bestrafte damit genau die Verbesserung,
|
||
die Filipe bestellt hatte.
|
||
|
||
NACHGESEHEN STATT GEGLAUBT: Auf der Seite stehen beide
|
||
Personenkacheln mit Namen, Rolle, Ring und Stand, dazu vier
|
||
Abschnitte. Sie ist voll. Nur eben kurz.
|
||
|
||
DESHALB DARF JEDE SEITE SAGEN, WORAN MAN SIEHT, DASS SIE DA IST:
|
||
eine Zeichenzahl, wo Text die Sache ist -- ein MERKMAL, wo Struktur
|
||
die Sache ist. Wer die Seite umbaut, zieht das Merkmal mit; wer sie
|
||
leerlaufen laesst, wird rot. Eine Zahl kann beides nicht
|
||
unterscheiden. */
|
||
for (const [name, pfad, keks, wieVoll] 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. */
|
||
/* Zwei Personenkacheln, nicht 400 Zeichen: Die Seite ist eine
|
||
Uebersicht ueber Menschen, kein Aufsatz. */
|
||
["entwicklung", "entwicklung.html", null, { merkmal: ".e-person", mindestens: 2 }],
|
||
["talente", "talente.html"],
|
||
/* UND EINMAL DIESELBE SEITE MIT ANDEREN AUGEN. "Deine Karte" gibt es
|
||
nur fuer die, ueber die geurteilt wird -- DogFather sieht sie nie.
|
||
Ohne diese Zeile waere der ganze Abschnitt nie auf einem Bildschirm
|
||
gewesen. */
|
||
["entwicklung-hand", "entwicklung.html", kHand,
|
||
{ merkmal: ".e-person", mindestens: 1 }],
|
||
]) {
|
||
melde("");
|
||
melde(`=== ${pfad} ===`);
|
||
for (const [breite, hoehe] of [[1440, 1000], [390, 844]]) {
|
||
const r = await ansehen(name, pfad, "CODE-DOGI-0001", breite, hoehe, keks,
|
||
wieVoll?.merkmal || null);
|
||
if (breite === 1440) { gesehen[name] = r.text; gesehen[name + ":karte"] = r.karte; }
|
||
const wo = `${breite} px`;
|
||
|
||
if (wieVoll?.merkmal) {
|
||
ok(r.merkmale >= wieVoll.mindestens,
|
||
`${wo}: die Seite ist gefüllt (${r.merkmale}x ${wieVoll.merkmal}, `
|
||
+ `mindestens ${wieVoll.mindestens}; ${r.text.length} Zeichen)`);
|
||
} else {
|
||
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.
|
||
|
||
AUS 15 WURDE 0 (15.09.2026) -- und das ist kein Aufweichen.
|
||
Fünfzehn war eine feste Schwelle für "die Seite hat wirklich
|
||
gerendert", und genau diese Aufgabe erledigt zwei Zeilen weiter
|
||
oben schon die Messung der Zeichenzahl. Als die sechs Fragen von
|
||
entwicklung.html auf ihre eigene Seite umzogen, blieben bei
|
||
390 px noch 14 Textstücke übrig -- alle lesbar, die Seite in
|
||
Ordnung, die Prüfung rot. Die Zahl hätte man auf 14 setzen
|
||
können; das wäre derselbe Fehler ein zweites Mal mit einer
|
||
anderen Zahl gewesen. */
|
||
ok(r.kontrast.gemessen > 0 && 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(" | ") : ""));
|
||
|
||
/* ==== KANN DER FARBLESER, WAS ER SOLL? (25.09.2026) ===========
|
||
|
||
Eine Messung, deren Werkzeug stumpf ist, meldet nicht „weiss
|
||
nicht", sondern eine Zahl -- und die sieht aus wie ein Befund.
|
||
Genau das ist mit `color(srgb ...)` passiert: dunkler Text auf
|
||
hellblauer Flaeche wurde als 1,07 gemeldet, weil die Flaeche
|
||
nicht gelesen werden konnte und die Messung beim fast schwarzen
|
||
Elternteil landete.
|
||
|
||
Die vier Proben laufen bei JEDER Seite mit; sie kosten nichts
|
||
und wuerden sofort rot, wenn jemand den Leser vereinfacht. Die
|
||
letzte ist die wichtigste: Ein unbekanntes Format muss NICHTS
|
||
liefern. Ein Leser, der im Zweifel Schwarz zurueckgibt, waere
|
||
schlimmer als der alte -- er wuerde nie wieder auffallen. */
|
||
const pr = r.kontrast.probe || {};
|
||
const gleich = (a, b) => Array.isArray(a) && a.length === 4
|
||
&& a.every((x, i) => Math.abs(x - b[i]) < 0.6);
|
||
ok(gleich(pr.rgb, [10, 18, 30, 1])
|
||
&& gleich(pr.rgba, [255, 255, 255, 0.5])
|
||
&& gleich(pr.srgb, [93.7, 199.6, 246.6, 1])
|
||
&& gleich(pr.srgbAlpha, [255, 0, 0, 0.25])
|
||
&& pr.unsinn === null,
|
||
`${wo}: der Farbleser kennt rgb, rgba, color(srgb) und sagt sonst nichts`
|
||
+ ` (${JSON.stringify(pr.srgb)})`);
|
||
/* 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(" | ") : ""));
|
||
}
|
||
}
|
||
|
||
/* =======================================================================
|
||
WAR DAS NEUE UEBERHAUPT AUF DEM BILDSCHIRM?
|
||
|
||
Die Messungen darueber sagen "nichts liegt uebereinander" und "alles
|
||
lesbar" -- und beides ist auch dann wahr, wenn das Gesuchte gar nicht
|
||
da war. Eine leere Seite erfuellt jede dieser Bedingungen.
|
||
|
||
Deshalb hier die Gegenfrage, und zwar in beide Richtungen: Das Neue
|
||
MUSS dort stehen, wo es hingehoert, und darf NICHT dort stehen, wo
|
||
es nicht hingehoert. Nur die zweite Haelfte beweist, dass die erste
|
||
etwas heisst.
|
||
======================================================================= */
|
||
melde("");
|
||
melde("=== Stand das Neue auch wirklich da? ===");
|
||
{
|
||
const t = gesehen.talente || "";
|
||
ok(/Vorlagentext/.test(t),
|
||
"Talente: der Knopf fuer den Vorlagentext stand auf der Stufe „Beobachtet“");
|
||
ok(/Ins Team holen/.test(t),
|
||
"Talente: „Ins Team holen · Zugang anlegen“ stand auf der Stufe „Probe“");
|
||
/* GEGENPROBE: Der Vorlagentext gehoert auf GENAU EINE Stufe. Stuende
|
||
er auf jeder, waere er auf vier von fuenf eine Ablenkung -- und die
|
||
Zeile darueber waere trotzdem gruen. */
|
||
/* ZEILENWEISE gezaehlt und nicht als Wort: "Der Vorlagentext liegt
|
||
bereit" ist der Satz des naechsten Schritts, der Knopf steht
|
||
allein auf seiner Zeile. Als Wort gezaehlt waeren es zwei, und die
|
||
Zeile haette einen Fehler gemeldet, den es nicht gibt. */
|
||
const knoepfe = (wort) => t.split("\n").filter((z) => z.trim() === wort).length;
|
||
ok(knoepfe("Vorlagentext") === 1,
|
||
`und zwar genau einmal, nicht bei jedem Kandidaten (${knoepfe("Vorlagentext")})`);
|
||
ok(t.split("Ins Team holen").length - 1 === 1,
|
||
`der Zugangsknopf ebenso (${t.split("Ins Team holen").length - 1})`);
|
||
|
||
/* ===== DER ROLLENNAME KOMMT ALS DATEN, NICHT AUS DEM SKRIPT ========
|
||
|
||
Seit dem 16.09.2026 steht der Name im Regeltext des Treffs nicht
|
||
mehr fest im Browser -- er kommt mit der Antwort des Servers. Der
|
||
Grund: Jede Datei unter workspace/ ist ohne Anmeldung abrufbar
|
||
(nachgemessen: `talente.js` liefert 200 und 25 KB Quelltext).
|
||
|
||
DAS BRAUCHT EINE PRUEFUNG, und zwar genau hier. Im HTML steht als
|
||
Ersatzfassung „jemandem aus dem Team" -- ein Satz, der auch ohne
|
||
Skript stimmt. Kommt der Name NICHT an, bleibt diese Fassung
|
||
stehen, die Seite sieht vollkommen richtig aus, und niemand merkt
|
||
es. Ein Platzhalter „…" waere aufgefallen; eine richtige Ersatz-
|
||
fassung faellt nicht auf. Deshalb wird nachgesehen, ob wirklich
|
||
der Name dasteht. */
|
||
const regeln = gesehen.regeln || "";
|
||
ok(regeln.length > 400, `Regeln: die Seite ist gefüllt (${regeln.length} Zeichen)`);
|
||
ok(/sag es einem \S+/.test(regeln),
|
||
"Regeln: der Rollenname ist im Kontaktsatz angekommen");
|
||
ok(!/jemandem aus dem Team/.test(regeln),
|
||
"die Ersatzfassung steht NICHT mehr da — sie wurde wirklich ersetzt");
|
||
ok(/Pause von bis zu \d+ Tagen/.test(regeln),
|
||
"und die Frist steht als Zahl darin, nicht als Platzhalter");
|
||
|
||
const hand = gesehen["entwicklung-hand"] || "";
|
||
const dogi = gesehen.entwicklung || "";
|
||
/* WENN ES SCHIEFGEHT, SOLL DIE MELDUNG SAGEN, WAS STATTDESSEN DA WAR.
|
||
"nicht gefunden" allein laesst offen, ob die Karte fehlt oder die
|
||
ganze Seite eine andere war. */
|
||
const obenLinks = (hand.split("\n").map((z) => z.trim()).filter(Boolean)[0] || "(leer)");
|
||
/* OHNE RUECKSICHT AUF GROSS UND KLEIN: innerText liefert, was der
|
||
Browser ZEIGT -- und .e-abschnitt ist in Grossbuchstaben gesetzt.
|
||
Im Quelltext steht "Deine Karte", auf dem Bildschirm "DEINE
|
||
KARTE". Eine Suche mit Ruecksicht darauf meldet "steht nicht da"
|
||
fuer etwas, das dasteht. */
|
||
ok(/Deine Karte/i.test(hand),
|
||
`Entwicklung: die rechte Hand sieht ihre eigene Karte (${JSON.stringify(gesehen["entwicklung-hand:karte"])} / oben: ${obenLinks})`);
|
||
ok(!/Deine Karte/i.test(dogi),
|
||
"und DogFather sieht sie nicht -- ueber ihn urteilt hier niemand");
|
||
ok(hand !== dogi && hand.length > 400,
|
||
`zwei verschiedene Bildschirme (${dogi.length} / ${hand.length} Zeichen) -- der Keks hat also gewirkt`);
|
||
/* UND KEIN NAME EINES BEURTEILERS. Was der Server nicht ausliefert,
|
||
kann die Seite nicht zeigen -- aber genau das ist die Behauptung,
|
||
und Behauptungen gehoeren gemessen. */
|
||
ok(!/Filipe/.test(hand),
|
||
"und in ihrer Karte steht kein Name dessen, der geurteilt hat");
|
||
}
|
||
|
||
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);
|