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]>
893 lines
39 KiB
JavaScript
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);
|