Files
dogfather-universe/server/mess-foyer.mjs
T
DogFatherGitandClaude Opus 5 0d52c4dc05 Das Regie-Tor: jeder sieht es, nur die zwei kommen rein -- und Farbe für die Reihen
Filipe, zum Bildschirmfoto des Foyers: „unter den [Reihen] fehlt eine
kachel, die viel kraesser und spezieller aussehen soll, wo nur
dogfather oder vanvan reinkommen. mit ihren zugangscodes für die seite.
und das soll die regie kachel sein. die muss wirklich ultra krass sein.
komplett crazy. die anderen kacheln aber auch gerne farbiger machen und
nicht so kaal und dunkel."

Auf die Rückfrage, wie fest das Schloss sein soll: „punkt 2 aber die
soll jeder sehen aber nur vanvan und dogfather sollen da rein kommen
bitte."

Haus: Team Dogi (crew.dogfather-universe.com). Die Agentur ist nicht
berührt -- pruef-haus-trennung 107/0.

DAS TOR

Eine vierte Kachel über die GANZE BREITE unter den drei Reihen. Das ist
die stärkste Aussage, die ein Raster treffen kann, und sie kostet keine
einzige Farbe. Ein Licht läuft in sieben Sekunden darüber -- flach und
schmal, wie der Schein einer Lampe über einem Mischpult. Kein Blinken:
dasselbe Signal mit doppelter Belastung für die Augen, und diese Seite
steht manchmal eine Stunde offen. prefers-reduced-motion bekommt den
Schein stehend, nicht gar keinen -- der Zustand muss auch dann zu
erkennen sein.

Drei Zustände, und jeder sieht anders aus: verschlossen rot mit
geschlossenem Bügel, aufgeschlossen grün mit aufspringendem Bügel, und
für alle anderen gedämpftes Grau ohne Lauflicht, mit „nicht erlaubt"
schon am Mauszeiger.

ZWEI SCHLÖSSER HINTEREINANDER, UND NUR EINES IST GEHEIM

(1) DIE ROLLE, und zwar VOR dem Code -- ohne ihn anzusehen. Das ist
    wichtiger, als es aussieht: Sonst könnte irgendwer im Haus mit
    Rateversuchen die `versuche`-Bremse für seine eigene IP vollaufen
    lassen und sich damit von der ANMELDUNG aussperren; beide zählen in
    derselben Tabelle. Geprüft mit Gegenprobe: Ein Gast schickt den
    RICHTIGEN Admin-Code, kommt nicht durch, und die Versuchszahl
    bleibt bei 0.
(2) DER CODE, geprüft mit `codeGeprueft()` -- neu in workspace.js,
    neben der Anmeldung und mit deren Rechenvorschrift, deren Vergleich
    in konstanter Zeit und deren Bremse. Ein zweiter Codevergleich in
    einem anderen Modul wäre der, der beim nächsten Umbau der
    scrypt-Parameter stehen bleibt.

Und er prüft GENAU DIESE PERSON. Die Anmeldung geht alle Personen einer
Rolle durch -- dort ist der Code die Kennung. Hier wäre das falsch:
VanVans Code öffnete DogFathers Tür, und im Protokoll stünde, ER sei
hineingegangen.

WAS DIE TÜR LEISTET UND WAS NICHT -- UND DASS ES DASTEHT

Filipes „Punkt 2" heißt: Der Code öffnet die Tür, danach ist die Regie
offen wie bisher. Die Routen der Sendung prüfen weiterhin nur die Rolle.
Das ist die bewusste Wahl und keine vergessene Stelle -- eine Sperre,
die mitten in einer Übertragung zuschnappen kann, richtet mehr Schaden
an, als sie verhindert.

Damit das niemand überschätzt, steht es als Satz IM FENSTER, nicht nur
im Quelltext: „Das hält einen neugierigen Blick auf, nicht jemanden, der
an deinem offenen Rechner sitzt." Eine Sicherung, die stärker aussieht,
als sie ist, ist schlechter als gar keine.

FARBE -- ABER NICHT AUF DER FLÄCHE

Filipe hatte recht, und der Grund war meiner: Beim Umbau auf das
Hausmaterial heute Vormittag habe ich die Farbe mit herausgenommen, weil
die alte Fassung sie auf der FLÄCHE trug -- und genau das machte den
Text schlecht lesbar. Richtig ist nicht „keine Farbe", sondern Farbe,
wo kein Text steht:

  * Jede Reihe hat ihren Ton (`--ton`, derselbe Griff, über den
    module.css das Kantenlicht legt): Bernstein für das, was ansteht
    -- dieselbe Farbe wie „überfällig" --, Blau für die Sendung, rot
    sobald sie läuft, Grün für das, was hereinkommt: dieselbe Farbe,
    die ein angenommener Vorschlag trägt. Die Farben SAGEN etwas.
  * Ein Band im Kopf jeder Tafel, die Überschrift in ihrem Ton, die
    Schilder passend. Vorher war jedes Schild blau, egal in welcher
    Reihe es stand -- zwei Farben nebeneinander, die nichts
    voneinander wussten.
  * Ein Streifen am Zeilenanfang statt eines eingefärbten Kastens.
    Drei Pixel an der Kante sagen dasselbe, und der Text steht
    weiterhin auf dem Grund, auf dem er gemessen wurde.

Die Kachel selbst trägt dieselbe Silhouette und dasselbe deckende
Material wie jede Karte im Haus (`.regie-tor` steht in der Modulliste in
module.css und in der Materialliste in start.css). „Krass" heißt hier
nicht „anders als das Haus" -- genau das stand heute Vormittag schon
einmal in foyer.css und war ein Fehler.

PROTOKOLLWÖRTER, DIE MAN LESEN KANN

`personen.js` baut den Anzeigetext aus dem Schlüssel: Unterstriche
werden Leerzeichen, nur der erste Buchstabe wird groß. Aus
`regie_code_falsch` wäre auf dem Bildschirm „Regie code falsch"
geworden. Die Regel, die daraus folgt: Hauptwort plus Mittelwort, nie
zwei Hauptwörter. Jetzt `regie_aufgeschlossen`, `regie_abgeschlossen`,
`regie_verweigert`, `regie_unbefugt`. Und das Detail war ein
ISO-Zeitstempel mitten in einer Zeile, die ein Mensch überfliegt --
jetzt steht dort „12 Stunden".

GEMESSEN

  pruef-reaktion          571, 0 Fehler (vorher 538) -- Abschnitt 21
  mess-foyer               96, 0 Fehler (vorher 70), 12 Bildschirmfotos
  pruef-handy-teamdogi    263 Seitenaufrufe, 0 Befunde
  pruef-breiten            23 auf 45 Seiten und fünf Breiten
  pruef-struktur          102 · pruef-css-klassen 37 · pruef-lesbarkeit 14
  pruef-bewegung            9 · pruef-tippziele 13 · pruef-deutsche-texte 12
  pruef-crew-adresse      173 · pruef-haus-trennung 107
  pruef-community-sicht    10 · pruef-sackgassen 14
  alle 0 Fehler

UND EINE LÜCKE, DIE DIE EIGENE MESSUNG GEFUNDEN HAT

Der Vorhang mit dem Codefeld schließt auf Esc und auf einen Druck
daneben, und der Finger steht im Feld -- für jemanden ohne Maus war er
trotzdem eine Falle: Die Tabulatortaste lief durch die Knöpfe DAHINTER
weiter, sichtbar war aber das Codefeld. Man tippt auf A und bekommt B,
nur eben mit der Tastatur.

Der erste Riegel legte `#foyer` still -- und die neue Messung fiel
sofort darüber: Die KOPFLEISTE steht außerhalb davon, der Fokus lief
weiter nach „Abmelden". Jetzt wird alles neben dem Vorhang stillgelegt,
ABGELEITET statt aufgezählt (`body.children`), und beim Schließen genau
das wieder freigegeben, was ich selbst gesetzt habe.

Und die Messung selbst war beim ersten Entwurf zu streng: Sie verlangte
„der Fokus bleibt IMMER im Fenster" und wurde rot, obwohl die Sperre
tadellos arbeitete -- am Ende des Tabulatorkreises gibt der Browser den
Fokus an seine eigene Leiste ab, im Dokument steht dann `body`. `body`
ist kein Bedienelement. Gefragt ist jetzt das Richtige: Wird je ein
BEDIENELEMENT außerhalb erreicht? Die Gegenprobe nennt es beim Namen
(`DRAUSSEN:zurueck`).

mess-foyer misst beide Hälften von Filipes Satz: dass ein Gast das Tor
SIEHT (und ein Druck ihm trotzdem kein Codefeld öffnet) und dass nur die
zwei HINEINKOMMEN. Dazu der ganze Weg im Browser: falscher Code
abgewiesen und Feld geleert, richtiger Code führt in den Saal, die
Freigabe gilt in einem neuen Fenster -- und VanVans Tür ist trotzdem
noch zu.

EIN FEHLALARM IN DER EIGENEN MESSUNG, BEHOBEN

mess-foyer suchte zuerst das WORT „Warteschlange" im Dokument eines
Gastes und fand es -- im unsichtbaren Gerüst der linken Reihe, wo es als
Überschrift steht. Zwei Gründe, warum das falsch war: Eine Beschriftung
ist keine Auskunft, und dass es eine Warteschlange GIBT, steht seit
heute im Regie-Tor, das jeder sieht. Eine Messung, die genau das als
Leck zählt, widerspricht dem Entwurf -- und sie hätte bei jedem Lauf
angeschlagen. Gefragt ist das Schärfere: Kommen DATEN an? Jetzt werden
Titel und Videokennungen geprüft, und dass keine einzige Planzeile
gebaut wurde.

NICHT BEHOBEN, WEIL NICHT MEINS: pruef-meldungen bleibt rot (28
Kennungen ohne Satz, 3 Rohanzeigen) -- gemessen im worktree auf dem
letzten Commit schon vorher, Zeichen für Zeichen dieselbe Liste. Meine
Arbeit hat 2 Kennungen und 2 Sätze ergänzt (174 -> 176, 155 -> 157), die
Zahlen gehen genau gleich hoch. Die Dateien gehören überwiegend der
Agentur; das gehört in einen eigenen Durchgang.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-08 12:13:22 +02:00

893 lines
39 KiB
JavaScript

/* =====================================================================
WIE DAS FOYER AUSSIEHT (08.10.2026)
`pruef-reaktion` misst, was die Schnittstelle antwortet. Das sagt
nichts darueber, ob die drei Reihen auf einem Bildschirm
nebeneinander passen, ob der Knopf in der Mitte ins Auge fiele und
ob am Telefon die Sendung oben steht.
WARUM EIGENES WERKZEUG UND KEIN ABSCHNITT IN mess-reaktion:
Dort laufen Kameras, Video und Chat -- jede geoeffnete Seite haelt
zwei stehende Verbindungen, und der Browser gibt je Adresse nur
wenige her. Das Foyer braucht keine davon. Es misst in Sekunden,
was dort zwischen zwei Kameraaufbauten untergehen wuerde.
GEMESSEN WIRD, WAS MAN NICHT ERRATEN KANN:
* Steht der Plan bei einem Gast wirklich NICHT da -- und steht
auch kein Stueck daraus irgendwo im Dokument?
* Ist die Mitte breiter als die Seiten, und rutschen die zwei
uebrigen Reihen auf, wenn der Plan fehlt?
* Steht am Telefon die Sendung in der ERSTEN Zeile?
* Ist der Knopf bei "zu" weg und bei "gleich"/"live" da?
* Sind alle Beruehrziele 44 Pixel hoch und alle Schriften
mindestens 11,5 Pixel gross?
* Und geht der ganze Weg durch: Gast schickt ein Video,
DogFather sieht es MIT Namen, nimmt es -- und es steht links
im Plan.
* Steht das Regie-Tor bei JEDEM da (Filipes ausdruecklicher
Wunsch) -- und kommt trotzdem nur durch, wer den richtigen
Code hat? Gemessen werden alle vier Lagen: Gast (kein Feld),
Host verschlossen, Host mit falschem Code, Host mit richtigem.
ZU JEDER MESSUNG, DIE "in Ordnung" SAGEN KANN, GEHOERT EINE
GEGENPROBE. Am Ende wird jede der tragenden Messungen absichtlich
kaputtgemacht und muss anschlagen. Eine Messung, die immer
bestaetigt, bestaetigt nichts.
NIE DIE ECHTE DATENBANK. Eigener Ordner, eigener Port, eigenes
Zertifikat -- und am Ende ist alles weg.
DREI AUSGAENGE:
0 in Ordnung
1 ein Befund
3 konnte nicht nachsehen (Playwright fehlt, kein Zertifikat,
die Buehne nimmt nichts an, oder der Stand hat sich waehrend
des Laufs geaendert)
Aufrufen mit: node server/mess-foyer.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { request } from "node:http";
import { createServer as httpsServer } from "node:https";
import { zertifikatBauen } from "./helfer-openssl.mjs";
import { notbremse } from "./helfer-notbremse.mjs";
import { eigenerPort } from "./helfer-port.mjs";
const HIER = dirname(fileURLToPath(import.meta.url));
const ordner = mkdtempSync(join(tmpdir(), "mess-foyer-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
const PORT = await eigenerPort(import.meta);
const HTTPS_PORT = await eigenerPort(import.meta, "mess-foyer (HTTPS)", 1);
process.env.PORT = `${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;
/* Der Sitzungskeks ist `secure`. Der Vorbau unten spricht https, der
Server dahinter nicht -- ohne diesen Griff setzt er den Keks und
der Browser verwirft ihn. */
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");
notbremse(240_000, "mess-foyer");
await new Promise((r) => setTimeout(r, 1000));
const CREW = "crew.dogfather-universe.com";
/* ---------- Leute ------------------------------------------------------- */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const kenn = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", kenn, new Date().toISOString());
const ALT = new Date(Date.now() - 200 * 86400000).toISOString();
function anlegen(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");
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", kenn).update(code).digest("hex"), ALT);
}
const CODES = {
admin: "CODE-M00-000001", hand: "CODE-M01-000001",
gast: "CODE-M02-000001", gast2: "CODE-M03-000001",
};
/* VANVAN IST DIE RECHTE HAND und wird mitgemessen, nicht nur
DogFather: Filipe hat "nur ich und vanvan" gesagt, und ob die
zweite Haelfte davon stimmt, sieht man nur, wenn sie dabei ist. */
anlegen("DogFather", "admin", CODES.admin);
anlegen("VanVan", "hand", CODES.hand);
anlegen("Nachtfalter", "gast", CODES.gast);
anlegen("Lenny", "gast", CODES.gast2);
d.close();
function roh(pfad, { method = "GET", keks = "", rumpf = null } = {}) {
return new Promise((fertig, schief) => {
const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf));
const a = request({
host: "127.0.0.1", port: PORT, path: pfad, method,
headers: {
Host: CREW, Origin: `https://${CREW}`,
...(keks ? { Cookie: keks } : {}),
...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
},
}, (antwort) => {
const teile = [];
antwort.on("data", (s) => teile.push(s));
antwort.on("end", () => {
const text = Buffer.concat(teile).toString("utf8");
let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ }
fertig({ code: antwort.statusCode, text, j, kopf: antwort.headers });
});
});
a.on("error", schief);
if (daten) a.write(daten);
a.end();
});
}
async function anmelden(wer) {
const a = await roh("/workspace/api/anmelden", {
method: "POST",
rumpf: { rolle: wer === "gast2" ? "gast" : wer, code: CODES[wer], alter_ok: true },
});
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
}
const k = {
admin: await anmelden("admin"),
hand: await anmelden("hand"),
gast: await anmelden("gast"),
gast2: await anmelden("gast2"),
};
/* ---------- Zaehlwerk --------------------------------------------------- */
let fehler = 0;
let geprueft = 0;
const ok = (b, t) => {
geprueft++;
console.log((b ? " ok " : " FEHL ") + t);
if (!b) fehler++;
};
function nichtNachsehbar(grund) {
console.log("\n -- NICHT NACHSEHBAR");
console.log(` ${grund}`);
console.log(" Das ist KEIN Befund am Code -- es heisst nur, dass");
console.log(" gerade niemand nachsehen konnte.\n");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(3);
}
/* ---------- Die Buehne fuellen ------------------------------------------ */
/* ZWEI STUECKE IN DER WARTESCHLANGE, EINES MIT TERMIN UND EINES OHNE.
Genau das ist die Aussage der linken Reihe ("was keinen hat, steht
unten"), und mit nur einem Eintrag liesse sie sich nicht messen. */
for (const adresse of ["https://youtu.be/9bZkp7q19f0",
"https://www.youtube.com/watch?v=kJQP7kiw5Fk"]) {
const a = await roh("/workspace/api/reaktion/liste", {
method: "POST", keks: k.admin, rumpf: { video: adresse },
});
if (a.code >= 400) nichtNachsehbar(`Die Warteschlange nimmt nichts an (HTTP ${a.code}).`);
}
const liste0 = (await roh("/workspace/api/reaktion", { keks: k.admin })).j?.liste || [];
if (liste0.length !== 2) {
nichtNachsehbar(`In der Warteschlange stehen ${liste0.length} Stuecke statt 2.`);
}
/* Der Termin liegt in der ZUKUNFT und wird relativ gerechnet. Ein
festes Datum waere eine Zeitbombe: Es ist richtig, bis der Kalender
es einholt -- und dann schlaegt "ueberfaellig" an, ohne dass etwas
kaputt ist. */
{
const a = await roh(`/workspace/api/reaktion/liste/${liste0[0].id}/wann`, {
method: "PUT", keks: k.admin,
rumpf: { wann: new Date(Date.now() + 3 * 86400000).toISOString() },
});
if (a.code >= 400) nichtNachsehbar(`Der Termin laesst sich nicht setzen (HTTP ${a.code}).`);
}
await roh("/workspace/api/reaktion", {
method: "PUT", keks: k.admin,
rumpf: {
titel: "Wir schauen den neuen Trailer",
video: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
beginnt_am: new Date(Date.now() + 7 * 60_000).toISOString(),
},
});
/* ---------- Der Vorbau -------------------------------------------------- */
const schluesselDatei = join(ordner, "p.key");
const zertDatei = join(ordner, "p.crt");
try {
zertifikatBauen(schluesselDatei, zertDatei, CREW);
} catch (f) {
nichtNachsehbar(`Kein Zertifikat zu bauen: ${f?.message}\n`
+ " openssl liegt bei Git in mingw64/bin -- im System-PATH\n"
+ " steht oft nur Git/cmd.");
}
const vorbau = httpsServer({
key: readFileSync(schluesselDatei), cert: readFileSync(zertDatei),
}, (von, nach) => {
const weiter = request({
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` },
}, (antwort) => {
nach.writeHead(antwort.statusCode || 502, antwort.headers);
antwort.pipe(nach);
});
weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } });
von.pipe(weiter);
});
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
const BASIS = `https://${CREW}:${HTTPS_PORT}`;
let pw = null;
try {
pw = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
} catch (f) {
nichtNachsehbar(`Playwright ist nicht da: ${f?.message}`);
}
const browser = await pw.chromium.launch({
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"],
});
async function alsWer(wer, breite, hoehe) {
const kontext = await browser.newContext({
viewport: { width: breite, height: hoehe },
/* MIT FINGER, WENN ES EIN HANDY IST. Ohne `hasTouch` meldet der
Browser `pointer: fine` -- und damit greift KEINE Regel aus
`@media (pointer: coarse)`. Dort stehen im ganzen Haus die
44-Pixel-Beruehrziele. Eine Handy-Messung mit Mauszeiger misst
eine Seite, die auf keinem Handy so aussieht. */
hasTouch: breite <= 860,
ignoreHTTPSErrors: true,
});
await kontext.addCookies((k[wer] || "").split("; ").filter(Boolean).map((z) => {
const [n, ...r] = z.split("=");
return { name: n, value: r.join("="), domain: CREW, path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
seite.on("pageerror", (f) => konsole.push(String(f?.message || f)));
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
seite.on("response", (a) => {
/* Vorschaubilder kommen von YouTube und sind hier nicht
erreichbar -- das ist die Umgebung und kein Befund. */
if (a.status() >= 400 && !/ytimg\.com/.test(a.url())) {
konsole.push(`${a.status()} ${a.url().replace(/^https?:\/\/[^/]+/, "")}`);
}
});
await seite.goto(`${BASIS}/workspace/reaktion-foyer.html`, { waitUntil: "networkidle" });
/* NICHT AUF EINE ZEIT WARTEN, SONDERN AUF DIE AUSKUNFT. Solange im
Kopf "Wird geladen" steht, ist der Stand noch nicht da -- eine
Messung davor misst den leeren Rahmen. */
await seite.waitForFunction(
() => !/Wird geladen/.test(document.getElementById("kopf-satz")?.textContent || ""),
null, { timeout: 15000 });
await seite.waitForTimeout(250);
return { kontext, seite, konsole };
}
async function bild(seite, name) {
await seite.screenshot({ path: join(HIER, `mess-foyer-${name}.png`), fullPage: true });
console.log(` Bild: server/mess-foyer-${name}.png`);
}
/* =====================================================================
DIE MESSUNGEN, ALLE IM BROWSER
Sie stehen als Funktionen da und nicht als Code im Ablauf: Nur so
kann die Gegenprobe am Ende DIESELBE Messung auf eine absichtlich
kaputte Seite anwenden. Zwei Abschriften derselben Frage waeren
zwei Antworten, und eine davon waere irgendwann aelter.
===================================================================== */
/** Welche Reihen stehen da, wo, wie breit -- und wie viele Spalten? */
const reihenLesen = (seite) => seite.evaluate(() => {
const da = (id) => {
const e = document.getElementById(id);
if (!e) return null;
/* `hidden` allein genuegt nicht: Eine Reihe kann auch durch
`display:none` am Stilblatt verschwinden. Gefragt ist, ob ein
Mensch sie sieht. */
if (!e.offsetParent) return null;
const r = e.getBoundingClientRect();
return { oben: Math.round(r.top), breit: Math.round(r.width), hoch: Math.round(r.height) };
};
return {
plan: da("reihe-plan"),
saal: da("reihe-saal"),
raten: da("reihe-raten"),
spalten: getComputedStyle(document.getElementById("reihen"))
.gridTemplateColumns.split(/\s+/).filter(Boolean).length,
};
});
/** Steht eines dieser Woerter irgendwo im Dokument? */
const leckLesen = (seite, woerter) => seite.evaluate((w) => {
const text = document.body.innerText + " " + document.body.innerHTML;
return w.filter((z) => z && text.includes(z));
}, woerter);
/** Das kleinste Beruehrziel, die kleinste Schrift, der Ueberstand. */
const massLesen = (seite) => seite.evaluate(() => {
const wurzel = document.getElementById("foyer");
const sichtbar = (e) => {
const r = e.getBoundingClientRect();
return r.width > 0 && r.height > 0 && e.offsetParent !== null;
};
let ziel = null;
for (const e of wurzel.querySelectorAll("a, button, input, select, textarea")) {
if (!sichtbar(e)) continue;
const r = e.getBoundingClientRect();
if (!ziel || Math.round(r.height) < ziel.hoch) {
ziel = {
hoch: Math.round(r.height), breit: Math.round(r.width),
was: e.id || e.className || e.tagName,
};
}
}
let schrift = null;
for (const e of wurzel.querySelectorAll("*")) {
if (!sichtbar(e)) continue;
/* NUR ELEMENTE, DIE SELBST TEXT TRAGEN. Sonst waere jede
Umhuellung mitgezaehlt, und die Messung sagte etwas ueber den
Kasten statt ueber die Schrift. */
if (![...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())) continue;
const gr = parseFloat(getComputedStyle(e).fontSize);
if (!schrift || gr < schrift.gr) schrift = { gr, was: e.id || e.className || e.tagName };
}
return {
ziel, schrift,
ueber: Math.max(0, document.documentElement.scrollWidth
- document.documentElement.clientWidth),
};
});
/** Was sagt das Regie-Tor? */
const torLesen = (seite) => seite.evaluate(() => {
const tor = document.getElementById('regie-tor');
if (!tor) return null;
const r = tor.getBoundingClientRect();
const t = (k) => tor.querySelector(k)?.textContent.trim() || '';
return {
da: !!tor.offsetParent,
darf: tor.dataset.darf,
offen: tor.dataset.offen,
titel: t('.regie-tor__titel'),
satz: t('.regie-tor__satz'),
marke: t('.regie-tor__marke'),
breit: Math.round(r.width),
hoch: Math.round(r.height),
oben: Math.round(r.top),
/* Die Farbe, die das Licht traegt -- sie ist die Aussage ueber
den Zustand und nicht nur Zierde. */
ton: getComputedStyle(tor).getPropertyValue('--ton').trim(),
vorhangDa: !document.getElementById('regie-vorhang').hidden,
};
});
/** Was steht in der Mitte? */
const mitteLesen = (seite) => seite.evaluate(() => {
const t = (id) => document.getElementById(id)?.textContent.trim() || "";
const knopf = document.getElementById("saal-knopf");
return {
lampe: t("lampe-wort"),
stand: document.getElementById("lampe")?.dataset.stand || "?",
titel: t("saal-titel"),
satz: t("saal-satz"),
knopfDa: !!(knopf && knopf.offsetParent),
knopfWort: t("saal-knopf-wort"),
knopfZiel: knopf?.getAttribute("href") || "",
};
});
/** Die Zeilen der linken Reihe. */
const planLesen = (seite) => seite.evaluate(() => [
...document.querySelectorAll("#plan-liste .plan-zeile")].map((z) => ({
titel: z.querySelector(".plan-zeile__titel")?.textContent.trim() || "",
wann: z.querySelector(".plan-zeile__wann")?.textContent.trim() || "",
termin: z.dataset.termin || "",
})));
/** Die Zeilen der rechten Reihe. */
const ratenLesen = (seite) => seite.evaluate(() => [
...document.querySelectorAll("#raten-liste .raten-zeile")].map((z) => ({
titel: z.querySelector(".raten-zeile__titel")?.textContent.trim() || "",
unter: z.querySelector(".raten-zeile__unter")?.textContent.trim() || "",
stand: z.dataset.stand || "",
knoepfe: [...z.querySelectorAll(".raten-knopf")].map((b) => b.textContent.trim()),
})));
/* =====================================================================
1. GESCHLOSSEN, AM RECHNER, ALS COMMUNITY
===================================================================== */
console.log("\n=== 1. Geschlossen, am Rechner, als Community ===");
{
const gast = await alsWer("gast", 1440, 900);
const r = await reihenLesen(gast.seite);
ok(r.plan === null, "Gast: die linke Reihe steht gar nicht da");
ok(r.saal !== null && r.raten !== null, "Gast: Mitte und Anraten stehen da");
ok(r.spalten === 2, `Gast: zwei Spalten, nicht drei (${r.spalten})`);
ok(!!(r.saal && r.raten) && r.saal.breit > r.raten.breit,
`Gast: die Mitte ist die breitere (${r.saal?.breit} zu ${r.raten?.breit} px)`);
const leck = await leckLesen(gast.seite,
[...liste0.map((z) => z.titel), ...liste0.map((z) => z.video)]);
ok(leck.length === 0, "Gast: kein Stueck aus der Warteschlange im Dokument"
+ (leck.length ? " -> " + leck.join(", ") : ""));
const m = await mitteLesen(gast.seite);
ok(m.lampe === "Geschlossen", `Gast: die Lampe sagt Geschlossen (${m.lampe})`);
ok(m.knopfDa === false, "Gast: bei zu gibt es keinen Knopf");
ok(m.satz.length > 20, `Gast: ein Satz erklaert den Zustand (${m.satz.length} Zeichen)`);
const mass = await massLesen(gast.seite);
ok(mass.ueber === 0, `Gast: nichts ragt seitlich hinaus (${mass.ueber} px)`);
ok(!!mass.schrift && mass.schrift.gr >= 11.5,
`Gast: kleinste Schrift ${mass.schrift?.gr} px (noetig 11,5) bei ${mass.schrift?.was}`);
ok(gast.konsole.length === 0, "Gast: nichts in der Konsole"
+ (gast.konsole.length ? " -> " + gast.konsole.slice(0, 3).join(" | ") : ""));
await bild(gast.seite, "1-zu-gast");
await gast.kontext.close();
}
/* =====================================================================
2. GESCHLOSSEN, DOGFATHER UND VANVAN
===================================================================== */
console.log("\n=== 2. Geschlossen, DogFather und VanVan ===");
for (const wer of ["admin", "hand"]) {
const chef = await alsWer(wer, 1440, 900);
const r = await reihenLesen(chef.seite);
ok(r.plan !== null, `${wer}: die linke Reihe steht da`);
ok(r.spalten === 3, `${wer}: drei Spalten (${r.spalten})`);
ok(!!(r.plan && r.saal && r.raten)
&& r.saal.breit > r.plan.breit && r.saal.breit > r.raten.breit,
`${wer}: die Mitte ist die breiteste `
+ `(${r.plan?.breit} / ${r.saal?.breit} / ${r.raten?.breit} px)`);
const p = await planLesen(chef.seite);
ok(p.length === 2, `${wer}: beide Stuecke stehen im Plan (${p.length})`);
ok(p[0]?.termin === "ja", `${wer}: das mit Termin steht oben (${p[0]?.termin || "ohne"})`);
ok(/\d{1,2}:\d{2}/.test(p[0]?.wann || ""),
`${wer}: beim ersten steht eine Uhrzeit (${p[0]?.wann})`);
ok(p[1]?.wann === "noch ohne Termin",
`${wer}: das ohne Termin steht unten und sagt das (${p[1]?.wann})`);
const mass = await massLesen(chef.seite);
ok(mass.ueber === 0, `${wer}: nichts ragt seitlich hinaus (${mass.ueber} px)`);
ok(chef.konsole.length === 0, `${wer}: nichts in der Konsole`
+ (chef.konsole.length ? " -> " + chef.konsole.slice(0, 3).join(" | ") : ""));
if (wer === "admin") await bild(chef.seite, "2-zu-dogfather");
await chef.kontext.close();
}
/* =====================================================================
3. GLEICH -- DER WARTEBEREICH IST OFFEN
===================================================================== */
console.log("\n=== 3. Gleich (Wartebereich) ===");
await roh("/workspace/api/reaktion/stand",
{ method: "POST", keks: k.admin, rumpf: { stand: "vorbereitung" } });
{
const gast = await alsWer("gast", 1440, 900);
const m = await mitteLesen(gast.seite);
ok(m.lampe === "Gleich", `die Lampe sagt Gleich (${m.lampe})`);
ok(m.knopfDa, "der Knopf steht da -- auch bei der Vorschau schon");
ok(m.knopfWort === "In den Wartebereich", `er heisst In den Wartebereich (${m.knopfWort})`);
ok(m.knopfZiel === "reaktion.html", `er fuehrt in den Saal (${m.knopfZiel})`);
ok(/Start um/.test(m.satz), `der Satz nennt die Startzeit (${m.satz})`);
ok(m.titel.includes("Trailer"), `der Titel der Sendung steht da (${m.titel})`);
await bild(gast.seite, "3-gleich-gast");
await gast.kontext.close();
}
/* =====================================================================
4. AUF SENDUNG
===================================================================== */
console.log("\n=== 4. Auf Sendung ===");
await roh("/workspace/api/reaktion/stand",
{ method: "POST", keks: k.admin, rumpf: { stand: "live" } });
{
const gast = await alsWer("gast", 1440, 900);
const m = await mitteLesen(gast.seite);
ok(m.lampe === "Auf Sendung", `die Lampe sagt Auf Sendung (${m.lampe})`);
ok(m.knopfWort === "Jetzt beitreten", `der Knopf heisst Jetzt beitreten (${m.knopfWort})`);
ok(/dieselbe Stelle/.test(m.satz), `der Satz verspricht den Gleichlauf (${m.satz})`);
await bild(gast.seite, "4-live-gast");
await gast.kontext.close();
}
/* =====================================================================
5. AM TELEFON -- DIE SENDUNG STEHT OBEN
===================================================================== */
console.log("\n=== 5. Am Telefon (390 px) ===");
{
const gast = await alsWer("gast", 390, 844);
const r = await reihenLesen(gast.seite);
ok(r.plan === null, "Gast am Telefon: keine linke Reihe");
ok(!!(r.saal && r.raten) && r.saal.oben < r.raten.oben,
`Gast am Telefon: die Sendung steht ueber dem Anraten `
+ `(${r.saal?.oben} vor ${r.raten?.oben} px)`);
const mass = await massLesen(gast.seite);
ok(mass.ueber === 0, `Gast am Telefon: nichts ragt seitlich hinaus (${mass.ueber} px)`);
ok(!!mass.ziel && mass.ziel.hoch >= 44,
`Gast am Telefon: kleinstes Beruehrziel ${mass.ziel?.hoch} px `
+ `(noetig 44) bei ${mass.ziel?.was}`);
ok(!!mass.schrift && mass.schrift.gr >= 11.5,
`Gast am Telefon: kleinste Schrift ${mass.schrift?.gr} px bei ${mass.schrift?.was}`);
await bild(gast.seite, "5-live-handy-gast");
await gast.kontext.close();
}
{
const chef = await alsWer("admin", 390, 844);
const r = await reihenLesen(chef.seite);
ok(!!(r.plan && r.saal && r.raten)
&& r.saal.oben < r.raten.oben && r.raten.oben < r.plan.oben,
"DogFather am Telefon: Sendung, Anraten, Plan -- in dieser Folge "
+ `(${r.saal?.oben} / ${r.raten?.oben} / ${r.plan?.oben} px)`);
ok(r.spalten === 1, `DogFather am Telefon: eine Spalte (${r.spalten})`);
const mass = await massLesen(chef.seite);
ok(mass.ueber === 0, `DogFather am Telefon: nichts ragt hinaus (${mass.ueber} px)`);
ok(!!mass.ziel && mass.ziel.hoch >= 44,
`DogFather am Telefon: kleinstes Beruehrziel ${mass.ziel?.hoch} px bei ${mass.ziel?.was}`);
await bild(chef.seite, "6-live-handy-dogfather");
await chef.kontext.close();
}
/* =====================================================================
6. DER GANZE WEG: ANRATEN, SEHEN, NEHMEN
Hier wird nicht die Schnittstelle gefragt, sondern GEKLICKT. Der
Unterschied ist nicht theoretisch: Ein Knopf, an dem nichts haengt,
antwortet in der Pruefung nebenan tadellos.
===================================================================== */
console.log("\n=== 6. Anraten, sehen, nehmen ===");
{
const gast = await alsWer("gast", 1440, 900);
await gast.seite.fill("#raten-video", "https://www.youtube.com/watch?v=aqz-KE-bpKQ");
await gast.seite.fill("#raten-notiz", "Das muessen wir zusammen sehen.");
await gast.seite.click("#raten-los");
await gast.seite.waitForFunction(
() => document.querySelectorAll("#raten-liste .raten-zeile").length === 1,
null, { timeout: 10000 });
const meine = await ratenLesen(gast.seite);
ok(meine.length === 1, `der Gast sieht seinen eigenen Vorschlag (${meine.length})`);
ok(meine[0]?.stand === "offen", `er steht auf offen (${meine[0]?.stand})`);
ok(meine[0]?.unter === "wartet",
`und sagt wartet -- ohne Namen, auch nicht den eigenen (${meine[0]?.unter})`);
ok(!meine[0]?.knoepfe.includes("Nehmen"), "der Gast hat keinen Nehmen-Knopf");
ok(meine[0]?.knoepfe.includes("\u00d7"), "aber er darf zurueckziehen, was noch offen ist");
const fuss = await gast.seite.textContent("#raten-fuss");
ok(/noch 4 Videos/.test(fuss || ""), `er erfaehrt, wie viele noch gehen (${fuss})`);
await bild(gast.seite, "7-angeraten-gast");
await gast.kontext.close();
}
{
const chef = await alsWer("admin", 1440, 900);
const alle = await ratenLesen(chef.seite);
ok(alle.length === 1, `DogFather sieht den Vorschlag (${alle.length})`);
ok(/Nachtfalter/.test(alle[0]?.unter || ""),
`und bei ihm steht, wer es war (${alle[0]?.unter})`);
ok(alle[0]?.knoepfe.includes("Nehmen") && alle[0]?.knoepfe.includes("Diesmal nicht"),
`er hat beide Knoepfe (${alle[0]?.knoepfe.join(" / ")})`);
const zahl = await chef.seite.textContent("#raten-zahl");
ok(/1 offen/.test(zahl || ""), `das Schild zaehlt die offenen (${zahl})`);
await bild(chef.seite, "8-angeraten-dogfather");
/* NEHMEN -- und links muss es ankommen. */
await chef.seite.click(".raten-knopf--ja");
await chef.seite.waitForFunction(
() => document.querySelectorAll("#plan-liste .plan-zeile").length === 3,
null, { timeout: 10000 });
const p = await planLesen(chef.seite);
ok(p.length === 3, `nach Nehmen stehen drei Stuecke im Plan (${p.length})`);
const danach = await ratenLesen(chef.seite);
ok(danach[0]?.stand === "uebernommen",
`der Vorschlag steht auf uebernommen (${danach[0]?.stand})`);
ok(/kommt dran/.test(danach[0]?.unter || ""),
`und sagt kommt dran (${danach[0]?.unter})`);
await bild(chef.seite, "9-genommen-dogfather");
await chef.kontext.close();
}
{
/* UND DER EINSENDER ERFAEHRT ES. Ohne diese Messung waere der Stand
nur im Pult richtig und beim Gast von gestern. */
const gast = await alsWer("gast", 1440, 900);
const meine = await ratenLesen(gast.seite);
ok(meine[0]?.stand === "uebernommen",
`der Gast sieht den neuen Stand (${meine[0]?.stand})`);
ok(!meine[0]?.knoepfe.includes("\u00d7"),
"und kann nichts mehr zurueckziehen, was entschieden ist");
await gast.kontext.close();
}
{
/* LENNY SIEHT NICHTS DAVON -- "nur ich und vanvan sehen sie",
gemessen und nicht nur gelesen. */
const lenny = await alsWer("gast2", 1440, 900);
const seine = await ratenLesen(lenny.seite);
ok(seine.length === 0, `Lenny sieht fremde Vorschlaege nicht (${seine.length})`);
const leck = await leckLesen(lenny.seite, ["Nachtfalter", "aqz-KE-bpKQ"]);
ok(leck.length === 0, "und auch nicht im Dokument"
+ (leck.length ? " -> " + leck.join(", ") : ""));
await lenny.kontext.close();
}
/* =====================================================================
7. DAS REGIE-TOR
Filipe: „die soll jeder sehen aber nur vanvan und dogfather sollen
da rein kommen bitte." Beide Haelften werden gemessen -- dass sie
jeder SIEHT und dass nur die zwei HINEINKOMMEN. Eine davon allein
waere keine Aussage.
===================================================================== */
console.log("\n=== 7. Das Regie-Tor ===");
{
const gast = await alsWer("gast", 1440, 900);
const t = await torLesen(gast.seite);
ok(!!t && t.da, "der Gast SIEHT das Tor");
ok(t?.darf === "nein", `fuer ihn steht darf=nein (${t?.darf})`);
ok(t?.marke === "Nur ihr zwei", `das Schild sagt es (${t?.marke})`);
ok(/nicht hinein|T\u00fcr zu/i.test(t?.satz || ""),
`und der Satz auch (${t?.satz})`);
/* ==== ES DARF KEINE DATEN VERRATEN ============================
Der erste Entwurf suchte hier auch das WORT „Warteschlange" --
und fand es, im unsichtbaren Geruest der linken Reihe, wo es als
Ueberschrift steht. Das war ein Fehlalarm aus zwei Gruenden:
* Eine Beschriftung ist keine Auskunft. Dass es eine
Warteschlange GIBT, steht ohnehin im Regie-Tor, und das
sieht seit heute jeder -- auf Filipes ausdruecklichen
Wunsch. Eine Messung, die genau dieses Wort als Leck
zaehlt, widerspricht dem Entwurf.
* Und sie haette bei jedem Lauf angeschlagen. Eine Warnung,
die immer kommt, nimmt irgendwann die echte mit.
Gefragt ist das Schaerfere: Kommen DATEN an? Titel und
Videokennungen aus der Warteschlange duerfen im Dokument eines
Gastes nicht vorkommen -- und gebaute Zeilen schon gar nicht. */
const leck = await leckLesen(gast.seite,
[...liste0.map((z) => z.titel), ...liste0.map((z) => z.video)]);
ok(leck.length === 0, "das Tor verraet keine Daten aus der Regie"
+ (leck.length ? " -> " + leck.join(", ") : ""));
const zeilen = await gast.seite.evaluate(() => ({
plan: document.querySelectorAll("#plan-liste .plan-zeile").length,
leerzeile: document.querySelectorAll("#plan-liste .plan-leer").length,
}));
ok(zeilen.plan === 0 && zeilen.leerzeile === 0,
`und es ist auch keine Planzeile gebaut worden `
+ `(${zeilen.plan} Zeilen, ${zeilen.leerzeile} Leerhinweise)`);
/* EIN DRUCK OEFFNET IHM KEIN FELD. Sonst waere eine Kachel, die
jeder sieht, eine Ratestelle fuer jeden im Haus. */
await gast.seite.click("#regie-tor");
await gast.seite.waitForTimeout(150);
const danach = await torLesen(gast.seite);
ok(danach?.vorhangDa === false, "ein Druck oeffnet ihm kein Codefeld");
await bild(gast.seite, "10-tor-gast");
await gast.kontext.close();
}
{
const chef = await alsWer("admin", 1440, 900);
const t = await torLesen(chef.seite);
ok(t?.darf === "ja", `DogFather darf (${t?.darf})`);
ok(t?.offen === "nein", `und es ist zu (${t?.offen})`);
ok(t?.marke === "Verschlossen", `das Schild sagt es (${t?.marke})`);
const r = await reihenLesen(chef.seite);
ok(!!t && t.breit > r.saal.breit,
`das Tor ist breiter als die breiteste Reihe (${t?.breit} zu ${r.saal?.breit} px)`);
ok(!!t && !!r.saal && t.oben > r.saal.oben,
`und es steht UNTER den Reihen (${t?.oben} nach ${r.saal?.oben} px)`);
/* ---- Falscher Code ---- */
await chef.seite.click("#regie-tor");
await chef.seite.waitForSelector("#regie-vorhang:not([hidden])", { timeout: 5000 });
ok(true, "ein Druck oeffnet ihm das Codefeld");
await bild(chef.seite, "11-tor-vorhang");
/* ==== DIE TABULATORTASTE DARF NICHT HINTER DEN VORHANG ========
Esc schliesst, ein Druck daneben schliesst, der Finger steht im
Feld -- und trotzdem war der Vorhang fuer jemanden ohne Maus
eine Falle: Die Tabulatortaste lief durch die Knoepfe DAHINTER
weiter, sichtbar war aber das Codefeld. Gemessen wird deshalb
nicht, ob `inert` gesetzt ist, sondern wo der Fokus LANDET. */
{
/* `body` IST KEIN BEDIENELEMENT, und das ist hier der Unterschied
zwischen einem Befund und einem Fehlalarm. Am Ende des
Tabulatorkreises gibt der Browser den Fokus an die Leiste des
Browsers ab; im Dokument steht dann `document.body`. Mein
erster Entwurf verlangte „immer drin" und wurde deshalb rot,
obwohl die Sperre tadellos arbeitete -- gemeldet wurde
„drin, body", und kein einziges Bedienelement ausserhalb.
Gefragt ist also: Wird je ein BEDIENELEMENT ausserhalb des
Fensters erreicht? Deshalb sagt die Messung auch, WELCHES --
ein Befund, der nur „draussen" meldet, schickt einen suchen. */
const woher = () => chef.seite.evaluate(() => {
const e = document.activeElement;
if (!e || e === document.body || e === document.documentElement) return "body";
return e.closest("#regie-vorhang")
? "drin"
: "DRAUSSEN:" + (e.id || e.className || e.tagName);
});
const wo = [];
for (let i = 0; i < 6; i++) { await chef.seite.keyboard.press("Tab"); wo.push(await woher()); }
ok(wo.includes("drin") && !wo.some((x) => x.startsWith("DRAUSSEN")),
`kein Bedienelement hinter dem Vorhang ist erreichbar `
+ `(6x Tab: ${[...new Set(wo)].join(", ")})`);
/* GEGENPROBE: Ohne die Stilllegung MUSS ein Element dahinter
auftauchen. Sonst bewiese die Zeile darueber nur, dass sechsmal
Tab irgendwo landet -- und nicht, dass die Sperre es ist. */
await chef.seite.evaluate(() => {
for (const e of document.body.children) e.inert = false;
});
const raus = [];
for (let i = 0; i < 8; i++) { await chef.seite.keyboard.press("Tab"); raus.push(await woher()); }
ok(raus.some((x) => x.startsWith("DRAUSSEN")),
`Gegenprobe: ohne die Sperre wird eines erreicht `
+ `(${raus.find((x) => x.startsWith("DRAUSSEN")) || [...new Set(raus)].join(", ")})`);
await chef.seite.evaluate(() => {
for (const e of document.body.children) {
if (e.id !== "regie-vorhang") e.inert = true;
}
});
}
await chef.seite.fill("#regie-code", "CODE-XXX-999999");
await chef.seite.click("#regie-los");
await chef.seite.waitForSelector("#regie-fehler:not([hidden])", { timeout: 8000 });
const meldung = await chef.seite.textContent("#regie-fehler");
ok(/Code stimmt nicht/.test(meldung || ""),
`ein falscher Code wird abgewiesen, in ganzen Saetzen (${meldung})`);
const geleert = await chef.seite.inputValue("#regie-code");
ok(geleert === "", "und das Feld ist danach leer");
const nochZu = await torLesen(chef.seite);
ok(nochZu?.offen === "nein", "das Tor bleibt zu");
/* ---- Richtiger Code ---- */
await chef.seite.fill("#regie-code", CODES.admin);
await Promise.all([
chef.seite.waitForURL("**/reaktion.html", { timeout: 15000 }),
chef.seite.click("#regie-los"),
]);
ok(/reaktion\.html/.test(chef.seite.url()),
`der richtige Code fuehrt in den Saal (${chef.seite.url().split("/").pop()})`);
await chef.kontext.close();
}
{
/* ---- Und danach ist es offen, auch in einem neuen Fenster ----
Das ist der Punkt der ganzen Freigabe: Sie haengt an der Person
und nicht an dem Reiter, in dem getippt wurde. */
const chef = await alsWer("admin", 1440, 900);
const t = await torLesen(chef.seite);
ok(t?.offen === "ja", `das Tor steht offen (${t?.offen})`);
ok(t?.marke === "Offen", `das Schild sagt es (${t?.marke})`);
ok(/Offen bis/.test(t?.satz || ""), `und bis wann (${t?.satz})`);
await bild(chef.seite, "12-tor-offen");
/* VanVan hat ihre eigene Tuer. Waere die Freigabe an der Rolle
festgemacht statt an der Person, stuende sie jetzt auch bei ihr
offen -- und im Protokoll staende, SIE sei hineingegangen. */
const van = await alsWer("hand", 1440, 900);
const tv = await torLesen(van.seite);
ok(tv?.darf === "ja", `VanVan darf auch (${tv?.darf})`);
ok(tv?.offen === "nein",
`aber ihre Tuer ist ihre eigene und noch zu (${tv?.offen})`);
await van.kontext.close();
/* ---- Wieder abschliessen ---- */
await chef.seite.evaluate(() => fetch('/workspace/api/reaktion/regie',
{ method: 'DELETE', credentials: 'same-origin' }));
await chef.seite.reload({ waitUntil: "networkidle" });
await chef.seite.waitForTimeout(400);
const zu = await torLesen(chef.seite);
ok(zu?.offen === "nein", `abschliessen geht auch wieder (${zu?.offen})`);
await chef.kontext.close();
}
/* =====================================================================
8. DIE GEGENPROBEN
Fuenf Messungen oben koennen "in Ordnung" sagen. Hier wird jede
absichtlich kaputtgemacht -- schlaegt sie nicht an, ist sie
wertlos, und alle gruenen Haken davor beweisen nichts.
===================================================================== */
console.log("\n=== 8. Gegenproben ===");
{
const chef = await alsWer("admin", 390, 844);
/* (a) Die Reihenfolge am Telefon. */
await chef.seite.addStyleTag({ content: ".foyer-reihe--saal { order: 9 !important; }" });
await chef.seite.waitForTimeout(120);
const verdreht = await reihenLesen(chef.seite);
ok(!(verdreht.saal.oben < verdreht.raten.oben),
`Gegenprobe: eine verdrehte Reihenfolge wird erkannt `
+ `(${verdreht.saal.oben} / ${verdreht.raten.oben} px)`);
/* (b) Ein zu kleines Beruehrziel. */
await chef.seite.addStyleTag({
content: "#raten-los { min-height: 0 !important; height: 20px !important;"
+ " padding: 0 !important; }",
});
await chef.seite.waitForTimeout(120);
const klein = await massLesen(chef.seite);
ok(!!klein.ziel && klein.ziel.hoch < 44,
`Gegenprobe: ein 20-px-Knopf wird gefunden (${klein.ziel?.hoch} px)`);
/* (c) Eine zu kleine Schrift. */
await chef.seite.addStyleTag({ content: "#saal-satz { font-size: 9px !important; }" });
await chef.seite.waitForTimeout(120);
const winzig = await massLesen(chef.seite);
ok(!!winzig.schrift && winzig.schrift.gr < 11.5,
`Gegenprobe: 9-px-Schrift wird gefunden (${winzig.schrift?.gr} px)`);
/* (d) Und ein Ueberstand. */
await chef.seite.addStyleTag({ content: "#saal-titel { width: 900px !important; }" });
await chef.seite.waitForTimeout(120);
const breit = await massLesen(chef.seite);
ok(breit.ueber > 0, `Gegenprobe: ein Ueberstand wird gemessen (${breit.ueber} px)`);
await chef.kontext.close();
}
{
const gast = await alsWer("gast", 1440, 900);
/* (e) Das Leck. Ein Wort wird von Hand ins Dokument geschrieben --
findet die Messung es nicht, hat sie oben nichts geprueft. */
await gast.seite.evaluate(() => {
document.getElementById("saal-satz").textContent += " ZZTESTWORTZZ";
});
const leck = await leckLesen(gast.seite, ["ZZTESTWORTZZ"]);
ok(leck.length === 1, "Gegenprobe: ein Wort im Dokument wird gefunden");
/* (f) Die Reihen-Messung muss eine AUFGEDECKTE Reihe auch sichtbar
nennen -- sonst waere "der Plan steht nicht da" oben nur die
Folge davon, dass sie nie etwas findet. */
await gast.seite.evaluate(() => {
document.getElementById("reihe-plan").hidden = false;
});
await gast.seite.waitForTimeout(120);
const sichtbar = await reihenLesen(gast.seite);
ok(sichtbar.plan !== null, "Gegenprobe: eine aufgedeckte Reihe gilt als sichtbar");
/* (g) Und der Tor-Leser muss ein verschwundenes Tor auch
verschwunden nennen -- sonst waere „der Gast sieht es" oben nur
die Folge davon, dass er immer „da" sagt. */
await gast.seite.evaluate(() => {
document.getElementById("regie-tor").style.display = "none";
});
await gast.seite.waitForTimeout(120);
const weg = await torLesen(gast.seite);
ok(weg?.da === false, "Gegenprobe: ein ausgeblendetes Tor gilt als weg");
await gast.kontext.close();
}
/* =====================================================================
SCHLUSS
===================================================================== */
await browser.close();
await new Promise((r) => vorbau.close(r));
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
/* DIE ANZAHL GEHOERT IN DIE BEDINGUNG. Faende dieses Werkzeug keine
einzige Messung, waere "0 Fehler" das beste Ergebnis ueberhaupt --
und voellig wertlos. */
console.log(`\n${geprueft} Messungen, ${fehler} Fehler`);
if (geprueft < 55) {
console.log(`NICHT NACHSEHBAR: nur ${geprueft} Messungen gelaufen, erwartet mindestens 55.`);
process.exit(3);
}
if (fehler) {
console.log("NICHT IN ORDNUNG");
process.exit(1);
}
console.log("ALLES IN ORDNUNG");
process.exit(0);