Das Foyer: eine Seite vor dem Saal, mit Plan, Eintritt und Vorschlägen
Filipe: „diese kachel soll nicht mehr geschlossen sein, ABER wenn man
drauf drückt geht zuerst eine seite auf mit reihen/kategorien. links
eine reihe wo die nächsten geplanten videos schon bereit stehen. wan
und umd wie viel uhr … und das soll geschlossen sein außer für vanvan
und dogfather. in der mitte sollen die leute dan wenn eine sendung
läuft drauf drücken können und der sendung beitreten. auch bei der
vorschau schon und dan rechts will ich die letzte reihe/kategorie, da
sollen die leute alle mir videos anraten können … und nur ich und
vanvan sehen sie."
Haus: Team Dogi (crew.dogfather-universe.com). Die Agentur ist nicht
berührt -- pruef-haus-trennung 107/0, pruef-workspace-seiten 37/0.
WARUM EINE EIGENE SEITE UND KEIN VIERTER ZUSTAND DES SAALS
Der Saal meldet jeden an, der ihn öffnet („dabei"), baut Verbindungen
auf und hält den Ereignisstrom offen. Das ist richtig für jemanden,
der zusieht -- und falsch für jemanden, der nur nachsehen will, ob
heute Abend etwas läuft. Wäre das Foyer der vierte Zustand derselben
Seite, zählte jeder Blick als Zuschauer, und die Zahl im Regiepult
sähe nach Publikum aus, wo niemand zusieht.
Im Foyer wird nur gelesen: Stand holen, zeichnen, alle 20 Sekunden
nachfragen -- und nicht, solange der Reiter im Hintergrund liegt. Kein
Ereignisstrom: Der hielte eine Verbindung offen, und manche Browser
geben je Adresse nur sechs her; im Saal wird jede davon gebraucht.
DREI REIHEN
LINKS die Warteschlange, nach TERMIN sortiert statt nach Platz.
Was keinen hat, steht unten; was vorbei ist, bekommt
„· überfällig". Nur admin und rechte Hand -- und für alle
anderen steht sie gar nicht erst da, statt leer oder
gesperrt zu sein. Ein Kasten mit Schloss erzählt, dass es
etwas zu sehen gäbe. Entschieden wird das am Server; die
Seite fragt nicht nach der Rolle, sondern danach, ob die
Auskunft gekommen ist.
MITTE Lampe (Geschlossen / Gleich / Auf Sendung), Vorschaubild,
Titel, Satz -- und der Knopf, der NUR dasteht, wenn er auch
hineinführt. Er ist ein Link und kein Button: Wer ihn mit
der mittleren Maustaste antippt, bekommt einen neuen Reiter.
RECHTS Video anraten. Jeder darf schicken, niemand sieht fremde,
jeder sieht seine eigenen mit Stand. Vier Sperren, und jede
beantwortet etwas anderes: zu schnell (45 s), zu viele offen
(5 je Person), schon angeraten, steht schon bereit.
Am Telefon steht die MITTE oben. Dort ist „links" nur noch „vor allem
anderen", und das wäre die falsche Reihenfolge.
WAS SONST NOCH DAZUGEHÖRT
* Jede Zeile der Warteschlange im Saal hat jetzt ein Terminfeld.
* Die Kachel sagt nicht mehr „geschlossen" -- das Foyer ist immer
offen. „gleich" und „● live" bleiben: Sie sagen etwas, das man ohne
sie nicht weiß.
* reaktion.html steht in OHNE_KACHEL_UEBERALL. Die erlaubten Seiten
werden aus den Kachelzielen abgeleitet; als die Kachel aufs Foyer
zeigte, war der Saal plötzlich von keiner mehr genannt und
antwortete mit einer Umleitung.
DREI DOPPELTE ANTWORTEN BESEITIGT
* „Wie viele Vorschläge darf einer offen haben" stand VIERMAL:
dreimal als `je_person_max || 5` und einmal als Wort „fünf" im Satz
(plus ein fünftes Mal in meldung.js). Kommt jetzt vom Server.
* `maxlength="200"` im Formular neben VORSCHLAG_NOTIZ_MAX im Server.
Das Feld lernt die Grenze von der Auskunft.
* Die drei Foyer-Tafeln hatten eine eigene Fläche (Weiß mit 4,5 %) und
eine eigene Form. Beides gibt es im Haus längst: Material in
start.css (45 Klassen, gemessen), Form in module.css. Die eigene
Fläche war dazu fast durchsichtig -- über einem hellen
Hintergrundbild stand der Text ohne deckenden Grund. Gefunden hat
das nicht eine Prüfung, sondern das Bildschirmfoto.
EINE LÜCKE IN DER PRÜFUNG GESCHLOSSEN
In module.css steht dieselbe 48er-Klassenliste ACHTMAL. Sieben trugen
die Marke MODULLISTE und wurden verglichen; die achte -- die Umrandung
beim Tabben (:focus-visible) -- nicht. Sie war damit die Einzige, die
still hätte abweichen können, ausgerechnet bei der Regel, die nur
sieht, wer mit der Tastatur bedient. pruef-css-klassen erwartet jetzt
acht. Eine steigende Prüfzahl, und sie ist begründet.
UND EINER, DEN KEIN WERKZEUG FINDEN KANN
Der Satz oben lautete „Links steht, was ansteht; rechts kannst du
etwas anraten." Für die Community gibt es „links" gar nicht, und am
Telefon stehen die Reihen untereinander. Eine Richtung im Text ist
eine Aussage über das Aussehen -- und das ändert sich mit dem
Bildschirm und mit der Rolle. Jetzt steht dort, WAS geht, nicht WO.
GEMESSEN
mess-foyer (neu) 70 Messungen, 0 Fehler, 9 Bildschirmfotos
pruef-reaktion 538, 0 Fehler (vorher 478)
pruef-handy-teamdogi 263 Seitenaufrufe, 0 Befunde (vorher 244)
pruef-sackgassen 14, 0 Fehler
pruef-crew-adresse 173, 0 Fehler
pruef-haus-trennung 107, 0 Fehler
pruef-struktur 102, 0 Fehler
pruef-neue-seiten 109, 0 Fehler
pruef-portnummern 41, 0 Fehler
pruef-haus-seiten 38, 0 Fehler
pruef-workspace-seiten 37, 0 Fehler
pruef-breiten 45 Seiten auf fünf Breiten, 0 beanstandet
pruef-community-sicht 10, 0 Fehler
pruef-deutsche-texte 12, 0 Fehler
pruef-css-klassen grün
mess-foyer klickt den ganzen Weg durch: Ein Gast schickt ein Video,
DogFather sieht es mit Namen, nimmt es, und es muss links im Plan
stehen. Zum Schluss macht es sechs seiner Messungen absichtlich kaputt
und prüft, dass sie das merken.
NICHT BEHOBEN, WEIL NICHT MEINS: pruef-meldungen ist rot (28
Kennungen ohne Satz, 3 Rohanzeigen) -- gemessen im worktree auf dem
letzten Commit schon vorher, Zeichen für Zeichen dieselbe Liste. Die
Dateien gehören überwiegend der Agentur; das gehört in einen eigenen
Durchgang.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,681 @@
|
||||
/* =====================================================================
|
||||
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.
|
||||
|
||||
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 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. 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=== 7. 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");
|
||||
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 < 40) {
|
||||
console.log(`NICHT NACHSEHBAR: nur ${geprueft} Messungen gelaufen, erwartet mindestens 40.`);
|
||||
process.exit(3);
|
||||
}
|
||||
if (fehler) {
|
||||
console.log("NICHT IN ORDNUNG");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("ALLES IN ORDNUNG");
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user