DER BEFUND
Im naechtlichen Lauf standen dreizehn Pruefungen mit derselben Zeile
rot:
[uncaughtException] Error: spawnSync openssl ENOENT
Kein Programmfehler. Die Pruefungen brauchen einen https-Vorbau (der
Sitzungskeks des Hauses ist `secure` und kommt ueber http nicht an),
und dafuer erzeugen sie ein Wegwerf-Zertifikat mit openssl.
GEMESSEN, NICHT VERMUTET
openssl liegt hier: C:\Program Files\Git\mingw64\bin\openssl.exe
C:\Program Files\Git\usr\bin\openssl.exe
im System-PATH steht: C:\Program Files\Git\cmd (nur git.exe)
im Benutzer-PATH: nichts mit Git
Wer aus Git Bash startet, erbt die mingw-Pfade und merkt nie etwas.
Die Aufgabenplanung startet `node.exe` DIREKT, ohne Shell -- und
bekommt sie nicht. Bei mir gruen, nachts rot, und der Grund steht
nicht im Code.
Das Schlimmste daran ist nicht der Ausfall: Dreizehn dauerhaft rote
Zeilen in einer Notiz, die Filipe morgens liest, gewoehnen einem das
Hinsehen ab -- und decken dabei die echten Befunde zu.
DIE REPARATUR
`server/helfer-openssl.mjs` sucht openssl erst im PATH (ohne `where`
oder `which` -- beides sind selbst Programme und koennen genauso
fehlen), danach an den Orten, an denen es auf einem Windows-Rechner
mit Git wirklich liegt.
Dazu `zertifikatBauen(schluessel, zertifikat, host)`. Die acht
Aufrufformen im Haus waren identisch bis auf die Variablennamen
(schl/zert, schluesselDatei/zertDatei, zKey/zCrt, sk/zt, key/crt) --
eine Stelle traegt alle neunundzwanzig. Zwei Tage zuvor war eine
davon um ein `-addext` aermer als die anderen; gemerkt hat es
niemand, weil der Browser den fehlenden Alternativnamen erst bei
einer Weiterleitung anmahnt. Eine Stelle kann nicht von sich selbst
abweichen.
KEIN EINTRAG IN DEN SYSTEM-PATH. `mingw64\bin` enthaelt rund hundert
Programme mit Unix-Namen (`find`, `sort`, `link`), die gleichnamige
Windows-Befehle verdecken. Das fuer eine Pruefung zu aendern haette
an ganz anderen Stellen Fehler verursacht, die niemand hierher
zurueckverfolgt.
GEGENPROBE IN BEIDE RICHTUNGEN, mit dem PATH des Nachtlaufs:
vorher (direkter Aufruf): ABSTURZ: spawnSync openssl ENOENT
nachher (ueber den Helfer): Zertifikat gebaut, 1236 Bytes
Und eine echte Pruefung unter denselben Bedingungen:
PATH ohne mingw64 -> pruef-chat-neu-stelle
63 Pruefungen, 0 Fehler, 0 Abstuerze
Genau diese Datei stand im Nachtlauf mit ENOENT rot.
EINE WACHE DAGEGEN
pruef-struktur prueft ab jetzt, dass niemand openssl wieder direkt
ruft -- der naechste merkt es dort und nicht erst in einem
Nachtlauf, den niemand liest. Drei Gegenproben.
Beim ersten Anlauf schlug sie auf ihre EIGENEN Probetexte an: Sie
liest alle Serverdateien, und dazu gehoert sie selbst. Die Texte
werden jetzt zusammengesetzt. Derselbe Selbsttreffer ist mir heute
schon zweimal passiert.
DER DRITTE AUSGANG BLEIBT, WO ER HINGEHOERT
`helfer-kachel-echtfarbe.mjs` faengt den Fehler weiterhin ab und
meldet `moeglich: false` mit Grund, statt abzubrechen. Eine
Sicherung abzuschaffen, weil ihr Anlass gerade behoben ist, ist der
Anfang des naechsten stillen Fehlschlags.
NEBENBEI: pruef-community-sicht
Sie war rot mit "ZU VIEL: reaktion.html" -- mein eigener Rueckstand
vom 28.09. Die Reaction steht als Kachel im Community-Bereich; die
Liste war nicht nachgezogen. Jetzt 10 / 0.
Die Liste bleibt bewusst von Hand gepflegt: Sie ist eine ABSICHT,
keine Ableitung. Aus rechte.js gelesen verglichen sich zwei Kopien
derselben Quelle -- immer gruen, nie ein Beweis.
GEPRUEFT
pruef-struktur ALLES IN ORDNUNG (mit der neuen Wache)
pruef-community-sicht 10 / 0
pruef-chat-neu-stelle 63 / 0 (mit dem PATH des Nachtlaufs)
pruef-notizen, -teamlage-karten, -werdegang: EXIT 0, keine Abstuerze
mess-quer EXIT 0
29 Dateien: node --check auf allen, kein direkter Aufruf mehr
2573 lines
116 KiB
JavaScript
2573 lines
116 KiB
JavaScript
/* =====================================================================
|
||
WIE DIE REACTION AUSSIEHT (28.09.2026)
|
||
|
||
Die Pruefung nebenan misst, was die Schnittstelle antwortet. Das
|
||
sagt nichts darueber, ob ein Mensch damit einen Abend verbringen
|
||
moechte. Dieses Werkzeug oeffnet die Seite in einem echten Browser,
|
||
schaltet sie durch alle drei Zustaende und legt Bilder ab.
|
||
|
||
Gemessen wird dabei auch, was man sonst uebersieht:
|
||
- laeuft etwas in die Konsole?
|
||
- ragt etwas seitlich hinaus?
|
||
- ist der Saal so hoch wie das Fenster, oder muss man scrollen?
|
||
|
||
NIE DIE ECHTE DATENBANK. Eigener Ordner, eigener Port, eigenes
|
||
Zertifikat -- und am Ende ist alles weg.
|
||
|
||
Aufrufen mit: node server/mess-reaktion.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 { execFileSync } from "node:child_process";
|
||
import { createServer as httpsServer } from "node:https";
|
||
import { zertifikatBauen } from "./helfer-openssl.mjs";
|
||
|
||
const HIER = dirname(fileURLToPath(import.meta.url));
|
||
const ordner = mkdtempSync(join(tmpdir(), "mess-reakt-"));
|
||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||
|
||
const PORT = 5483;
|
||
const HTTPS_PORT = PORT + 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";
|
||
|
||
for (const art of ["uncaughtException", "unhandledRejection"]) {
|
||
process.on(art, (f) => {
|
||
console.error(`\nABBRUCH (${art}): ${f?.stack || f}`);
|
||
process.exit(8);
|
||
});
|
||
}
|
||
|
||
const express = (await import("express")).default;
|
||
const ec = express.response.cookie;
|
||
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
||
await import("./index.js");
|
||
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);
|
||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
}
|
||
const CODES = {
|
||
admin: "CODE-M00-000001", gast: "CODE-M01-000001", gast2: "CODE-M02-000001",
|
||
modi: "CODE-M03-000001", linke: "CODE-M04-000001",
|
||
};
|
||
const IDS = {
|
||
admin: anlegen("DogFather", "admin", CODES.admin),
|
||
gast: anlegen("Nachtfalter", "gast", CODES.gast),
|
||
gast2: anlegen("Lenny", "gast", CODES.gast2),
|
||
/* Fuer die Moderation. Sie ist der Teil, bei dem ein Fehler am
|
||
meisten kostet -- also wird sie im Browser gemessen und nicht
|
||
nur ueber die Schnittstelle. */
|
||
modi: anlegen("Miss", "modi", CODES.modi),
|
||
linke: anlegen("Funny", "linke", CODES.linke),
|
||
};
|
||
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(rolle) {
|
||
const a = await roh("/workspace/api/anmelden", {
|
||
method: "POST",
|
||
rumpf: { rolle: rolle === "gast2" ? "gast" : rolle, code: CODES[rolle], alter_ok: true },
|
||
});
|
||
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
|
||
}
|
||
const k = {
|
||
admin: await anmelden("admin"),
|
||
gast: await anmelden("gast"),
|
||
gast2: await anmelden("gast2"),
|
||
modi: await anmelden("modi"),
|
||
linke: await anmelden("linke"),
|
||
};
|
||
|
||
/* ---------- Die Buehne fuellen ------------------------------------------ */
|
||
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(),
|
||
},
|
||
});
|
||
|
||
/* ZWEI STUECKE IN DIE WARTESCHLANGE. Eine leere Liste sieht auf
|
||
einem Bildschirmfoto genauso aus wie eine kaputte -- beides ist
|
||
ein Kasten ohne Zeilen. Zwei echte Eintraege zeigen, ob Titel,
|
||
Vorschaubild und die vier Knoepfe je Zeile wirklich ankommen. */
|
||
for (const adresse of ["https://youtu.be/9bZkp7q19f0",
|
||
"https://www.youtube.com/watch?v=kJQP7kiw5Fk"]) {
|
||
await roh("/workspace/api/reaktion/liste", {
|
||
method: "POST", keks: k.admin, rumpf: { video: adresse },
|
||
});
|
||
}
|
||
|
||
const REDEN = [
|
||
["gast", "Bin schon daaa 🐺"],
|
||
["gast2", "Ich auch! Hab extra Popcorn geholt"],
|
||
["gast", "Wie lange noch?"],
|
||
["admin", "Zwei Minuten, ich richte noch die Kamera"],
|
||
/* DAS TEAM SCHREIBT MIT. Ohne einen Beitrag von einer linken Hand
|
||
und einem Modi liesse sich gar nicht messen, ob die Abzeichen
|
||
ankommen -- und eine Messung, die ihren Gegenstand nicht
|
||
herstellt, misst nichts. */
|
||
["linke", "Ich pass auf den Chat auf, schreibt ruhig"],
|
||
["modi", "Und ich auch 🙂"],
|
||
["gast2", "Kein Stress, wir sitzen bequem"],
|
||
["gast", "Letztes Mal war so gut, ich hab geheult"],
|
||
];
|
||
|
||
/* ---------- Der Vorbau -------------------------------------------------- */
|
||
const schluesselDatei = join(ordner, "p.key");
|
||
const zertDatei = join(ordner, "p.crt");
|
||
zertifikatBauen(schluesselDatei, zertDatei, CREW);
|
||
|
||
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}`;
|
||
|
||
const pw = await import(
|
||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||
const browser = await pw.chromium.launch({
|
||
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors",
|
||
/* Kamera und Mikrofon ohne Rueckfrage -- und ein bewegtes
|
||
Testbild statt eines schwarzen Rechtecks. Ohne das sieht jede
|
||
Aufnahme aus, als waere die Kamera kaputt. */
|
||
"--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"],
|
||
});
|
||
|
||
/* JEDE GEOEFFNETE SEITE HAELT ZWEI STEHENDE VERBINDUNGEN (der
|
||
Ereignisstrom der Kopfleiste und der dieser Seite). Ein Browser
|
||
laesst je Adresse nur wenige davon zu -- wer seine Seiten nicht
|
||
schliesst, nimmt den spaeteren den Platz weg.
|
||
|
||
GEMESSEN: Die Seiten aus den ersten beiden Abschnitten blieben
|
||
offen, und danach kam Lennys Strom gar nicht mehr zustande. Die
|
||
Signale gingen ins Leere, und die Kamerafenster blieben schwarz --
|
||
ein Messfehler, der wie ein Programmfehler aussah. */
|
||
async function alsWer(rolle, 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 der Regeln aus `@media (pointer: coarse)`.
|
||
Dort stehen im ganzen Haus die 44-Pixel-Beruehrziele und seit
|
||
dem 28.09. auch das Ausblenden der Tastenkuerzel. Eine
|
||
Handy-Messung mit Mauszeiger misst eine Seite, die auf keinem
|
||
Handy so aussieht -- und meldet Hoehen, die dort andere sind. */
|
||
hasTouch: breite <= 860,
|
||
ignoreHTTPSErrors: true,
|
||
permissions: ["camera", "microphone"],
|
||
});
|
||
await kontext.addCookies((k[rolle] || "").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 = [];
|
||
const anfragen = [];
|
||
seite.on("request", (r) => { if (/\/api\//.test(r.url())) anfragen.push(r.url().replace(/^https?:\/\/[^/]+/, "")); });
|
||
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) => {
|
||
if (a.status() >= 400) konsole.push(`${a.status()} ${a.url().replace(/^https?:\/\/[^/]+/, "")}`);
|
||
});
|
||
return { kontext, seite, konsole, anfragen };
|
||
}
|
||
|
||
async function bild(seite, name) {
|
||
await seite.screenshot({ path: join(HIER, `mess-reaktion-${name}.png`) });
|
||
console.log(` Bild: server/mess-reaktion-${name}.png`);
|
||
}
|
||
|
||
async function vermessen(seite, wo) {
|
||
const m = await seite.evaluate(() => ({
|
||
ueber: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth),
|
||
hoch: Math.max(0, document.documentElement.scrollHeight - document.documentElement.clientHeight),
|
||
stand: document.getElementById("saal")?.dataset.stand || "?",
|
||
schild: document.getElementById("schild-wort")?.textContent || "",
|
||
}));
|
||
console.log(` ${wo}: Stand „${m.stand}", Schild „${m.schild}", `
|
||
+ `seitlich ${m.ueber} px, senkrecht ${m.hoch} px`);
|
||
return m;
|
||
}
|
||
|
||
console.log("\n=== ZU ===");
|
||
{
|
||
const { kontext, seite, konsole } = await alsWer("gast", 1440, 900);
|
||
await seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(900);
|
||
await vermessen(seite, "Rechner");
|
||
await bild(seite, "1-zu-rechner");
|
||
if (konsole.length) console.log(" KONSOLE:", konsole.slice(0, 3).join(" | "));
|
||
await kontext.close();
|
||
}
|
||
|
||
console.log("\n=== VORBEREITUNG ===");
|
||
await roh("/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "vorbereitung" } });
|
||
for (const [wer, text] of REDEN) {
|
||
await roh("/workspace/api/reaktion/chat", { method: "POST", keks: k[wer], rumpf: { text } });
|
||
}
|
||
{
|
||
const { kontext, seite, konsole } = await alsWer("gast", 1440, 900);
|
||
await seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(1400);
|
||
await vermessen(seite, "Rechner");
|
||
await bild(seite, "2-warte-rechner");
|
||
if (konsole.length) console.log(" KONSOLE:", konsole.slice(0, 3).join(" | "));
|
||
await kontext.close();
|
||
}
|
||
|
||
console.log("\n=== LIVE ===");
|
||
await roh("/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "live" } });
|
||
await roh("/workspace/api/reaktion/gast", { method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast2 } });
|
||
|
||
/* ==== BEIDE SEITEN GLEICHZEITIG ====================================
|
||
Der Host sendet, die Zuschauerin empfaengt -- das geht nur, wenn
|
||
beide zur selben Zeit offen sind. */
|
||
const host = await alsWer("admin", 1440, 900);
|
||
await host.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" });
|
||
await host.seite.waitForTimeout(2500);
|
||
|
||
const gast2 = await alsWer("gast2", 1280, 800);
|
||
await gast2.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" });
|
||
await gast2.seite.waitForTimeout(2500);
|
||
|
||
const zuschauer = await alsWer("gast", 1440, 900);
|
||
await zuschauer.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" });
|
||
|
||
/* Verbindungen brauchen ein paar Sekunden: Adressen holen, Angebot,
|
||
Antwort, Kandidaten. Acht Sekunden sind grosszuegig -- es geht
|
||
darum, ob es UEBERHAUPT ankommt, nicht wie schnell. */
|
||
await zuschauer.seite.waitForTimeout(8000);
|
||
|
||
async function bildBreiten(seite) {
|
||
return seite.evaluate(() => {
|
||
const raus = {};
|
||
for (const k2 of document.querySelectorAll("#kameras .kamera")) {
|
||
const v = k2.querySelector("video");
|
||
raus[k2.querySelector(".kamera__name")?.textContent || k2.dataset.wer] = {
|
||
breite: v ? v.videoWidth : -1,
|
||
strom: !!(v && v.srcObject),
|
||
laeuft: !!(v && !v.paused),
|
||
stumm: !!(v && v.muted),
|
||
};
|
||
}
|
||
return raus;
|
||
});
|
||
}
|
||
|
||
console.log("\n --- Fliesst ein Bild? ---");
|
||
for (const [wer, w] of [["Host", host.seite], ["Gast", gast2.seite], ["Zuschauerin", zuschauer.seite]]) {
|
||
const z = await w.evaluate(() => ({
|
||
url: location.pathname,
|
||
saal: !!document.getElementById("saal"),
|
||
stand: document.getElementById("saal")?.dataset.stand,
|
||
kameras: document.querySelectorAll("#kameras .kamera").length,
|
||
})).catch((f) => ({ url: "FEHLER " + f.message }));
|
||
console.log(` ${wer.padEnd(12)} ${z.url} saal=${z.saal} stand=${z.stand} kameras=${z.kameras}`);
|
||
const q = { Host: host, Gast: gast2, Zuschauerin: zuschauer }[wer];
|
||
console.log(" Strom-Anfragen: "
|
||
+ q.anfragen.filter((x) => x.includes("strom")).length + " | Signale: " + q.anfragen.filter((x) => x.includes("signal")).length);
|
||
}
|
||
const beiZuschauer = await bildBreiten(zuschauer.seite);
|
||
for (const [name, m] of Object.entries(beiZuschauer)) {
|
||
const gut = m.breite > 0;
|
||
console.log(` Zuschauerin sieht ${name.padEnd(12)} `
|
||
+ `${gut ? "BILD " + m.breite + " px" : (m.strom ? "Strom da, aber 0 px" : "NICHTS")}`
|
||
+ ` ${m.laeuft ? "laeuft" : "STEHT"} ${m.stumm ? "stumm" : "mit Ton"}`);
|
||
}
|
||
/* AUCH DER HOST MUSS SEINEN GAST SEHEN. Wenn nur die Zuschauer
|
||
etwas sehen, redet der Host gegen eine schwarze Kachel -- und
|
||
merkt nicht, wenn beim Gast die Kamera aus ist. */
|
||
/* LAEUFT DAS VIDEO UEBERHAUPT? Dieselbe Falle wie bei den Kameras:
|
||
Etwas, das Ton macht, darf nicht von allein losgehen. */
|
||
let videoLaeuft = 0;
|
||
for (const [wer, w] of [["Host", host.seite], ["Zuschauerin", zuschauer.seite]]) {
|
||
const v = await w.evaluate(() => ({
|
||
stand: document.getElementById("video-platz")?.dataset.spieler || "(nichts)",
|
||
rahmen: !!document.querySelector("#video-platz iframe"),
|
||
}));
|
||
console.log(` Video bei ${wer.padEnd(12)} Rahmen=${v.rahmen} Zustand=${v.stand}`);
|
||
if (v.stand === "laeuft" || v.stand === "puffert") videoLaeuft++;
|
||
}
|
||
const beimHost = await bildBreiten(host.seite);
|
||
for (const [name, m] of Object.entries(beimHost)) {
|
||
console.log(` Host sieht ${name.padEnd(12)} `
|
||
+ `${m.breite > 0 ? "BILD " + m.breite + " px" : "NICHTS"}`
|
||
+ ` ${m.laeuft ? "laeuft" : "STEHT"}`);
|
||
}
|
||
const zaehlt = Object.values(beiZuschauer).filter((m) => m.breite > 0).length;
|
||
console.log(` -> ${zaehlt} von ${Object.keys(beiZuschauer).length} Kameras kommen an.`);
|
||
/* DIESE MESSUNG HAT EINEN RUECKGABEWERT -- und zwar wegen dieser
|
||
einen Zahl. Alles andere kann gruen sein: Die Schnittstelle
|
||
antwortet, die Seite sieht gut aus, die Verbindung steht. Wenn
|
||
hier 0 steht, sitzt trotzdem jeder vor einem schwarzen Rechteck.
|
||
|
||
Gemessen wurde genau das zweimal: einmal, weil das Angebot
|
||
hinausging, bevor der Empfaenger zuhoeren konnte -- einmal, weil
|
||
ein Video mit Ton nicht von allein startet. Beide Male sahen die
|
||
Bildschirmfotos plausibel aus. */
|
||
let ausgang = 0;
|
||
/* BEKANNTE, AUFGESCHRIEBENE OFFENE PUNKTE.
|
||
Sie sind keine Fehler dieses Laufs und kein bestandener Test --
|
||
sie sind der dritte Ausgang: Mangel bekannt, Weg beschrieben,
|
||
noch nicht gegangen. Am Ende stehen sie gesondert. */
|
||
const offenePunkte = [];
|
||
/* LAEUFT DAS VIDEO? Ein Zuschauer hat keine Bedienung -- startet es
|
||
nicht von selbst, sitzt er eine Stunde vor einem Standbild und
|
||
kann nichts dagegen tun. Gemessen war genau das der Fall. */
|
||
if (videoLaeuft < 2) {
|
||
console.log(` ACHTUNG: Das Video laeuft nur bei ${videoLaeuft} von 2.`);
|
||
ausgang = 1;
|
||
}
|
||
const beimHostAn = Object.values(beimHost).filter((m) => m.breite > 0).length;
|
||
if (beimHostAn < Object.keys(beimHost).length) {
|
||
console.log(" ACHTUNG: Der Host sieht nicht alle Kameras.");
|
||
ausgang = 1;
|
||
}
|
||
if (!zaehlt) {
|
||
console.log(" ACHTUNG: Es kommt kein einziges Bild an.");
|
||
ausgang = 1;
|
||
}
|
||
const stehen = [...Object.values(beiZuschauer), ...Object.values(beimHost)]
|
||
.filter((m) => m.breite > 0 && !m.laeuft).length;
|
||
if (stehen) {
|
||
console.log(` ACHTUNG: ${stehen} Bild(er) kommen an, stehen aber still `
|
||
+ "-- ein Video mit Ton startet nicht von allein.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ---- Die Regie ---------------------------------------------------
|
||
NICHT UMSCHALTEN, SONDERN HERSTELLEN. Ein Klick auf „Griff" heisst
|
||
„das Gegenteil von dem, was gerade ist" -- und setzt damit voraus,
|
||
dass die Messung den Zustand vorher kennt. Genau daran ist sie
|
||
einmal gescheitert: Seit das Pult aufgeklappt startet, hat der
|
||
Klick es ZUgemacht, und auf dem Bild war von der Regie nichts zu
|
||
sehen. */
|
||
console.log("");
|
||
console.log(" --- Die Regie ---");
|
||
await host.seite.evaluate(() => {
|
||
const p = document.getElementById("pult");
|
||
if (p && p.dataset.auf !== "ja") document.getElementById("pult-griff")?.click();
|
||
});
|
||
await host.seite.waitForTimeout(900);
|
||
|
||
/* ---- Der Regieplatz: drei Kacheln und die Leiste unten ----------
|
||
|
||
Filipe: „kann das nicht bissl aufgeteilt sein auf links und rechts
|
||
und eine barre unten in der mitte."
|
||
|
||
GEMESSEN WIRD DIE LAGE UND NICHT DIE REGEL. Dass im Stilblatt
|
||
`grid-template-areas` steht, sagt nichts darueber, ob die rechte
|
||
Spalte auch wirklich rechts landet -- eine spaetere Regel, eine zu
|
||
breite Zelle, ein langer Titel, und sie steht darunter. Geprueft
|
||
wird deshalb an den Kanten:
|
||
linke Spalte endet, BEVOR die rechte anfaengt
|
||
die Leiste liegt UNTER beiden und ist so breit wie beide
|
||
Und zusaetzlich, dass nichts seitlich hinauslaeuft: Der Umbau hat
|
||
eine 16-rem-Spalte gegen einen Videotitel ohne Grenze gestellt. */
|
||
{
|
||
const platz = await host.seite.evaluate(() => {
|
||
const k = (w) => {
|
||
const e = document.querySelector(w);
|
||
if (!e) return null;
|
||
const r = e.getBoundingClientRect();
|
||
return {
|
||
l: Math.round(r.left), r: Math.round(r.right),
|
||
o: Math.round(r.top), u: Math.round(r.bottom),
|
||
b: Math.round(r.width), h: Math.round(r.height),
|
||
};
|
||
};
|
||
const innen = document.getElementById("pult-innen");
|
||
return {
|
||
/* DREI KACHELN, NICHT ZWEI SEITEN. In der 360 Pixel schmalen
|
||
Regiespalte stehen sie UNTEREINANDER -- das ist seit dem
|
||
Umbau „Regie links" der Sinn, nicht ein Fehler. Gefragt
|
||
ist deshalb: Gibt es alle drei, und hat jede Inhalt? */
|
||
laeuft: k(".regiekachel.kachel--laeuft"),
|
||
sendung: k(".regiekachel.kachel--sendung"),
|
||
bereit: k(".regiekachel.kachel--bereit"),
|
||
leiste: k(".transport"),
|
||
spiel: k("#v-spiel"),
|
||
/* Laeuft etwas seitlich hinaus? `scrollWidth` groesser als die
|
||
sichtbare Breite heisst genau das. */
|
||
ueber: innen ? innen.scrollWidth - innen.clientWidth : -1,
|
||
/* ==== UND IST DIE LEISTE UEBERHAUPT ZU SEHEN? ============
|
||
Beim ersten Bau lag sie bei y=986 in einem 900 Pixel hohen
|
||
Fenster -- die Rechnung ging auf, und man kam trotzdem nur
|
||
mit Scrollen an den Play-Knopf. Geprueft wird deshalb
|
||
gegen den SICHTBAREN Ausschnitt der Regie, nicht gegen die
|
||
Seite. */
|
||
sichtbar: (() => {
|
||
/* GEGEN DAS FENSTER, NICHT GEGEN DIE REGIE.
|
||
Die erste Fassung fragte, ob die Leiste innerhalb von
|
||
`#pult-innen` liegt. Seit dem 28.09. liegt sie gar nicht
|
||
mehr in der Regie, sondern als eigene Zeile UNTER dem
|
||
ganzen Saal -- die Antwort war also immer „nein", und
|
||
die Messung meldete dauerhaft einen Fehler, den es nicht
|
||
gab. Gefragt ist, was Filipe im Live angeht: Komme ich
|
||
an den Play-Knopf, ohne zu scrollen? */
|
||
const e = document.querySelector(".transport");
|
||
if (!e) return false;
|
||
const r = e.getBoundingClientRect();
|
||
return r.bottom <= window.innerHeight + 1 && r.top >= -1;
|
||
})(),
|
||
/* ==== UND SIE IST DIE LETZTE ZEILE ========================
|
||
„Unten" laesst sich bei einer KLEBENDEN Leiste nicht mehr an
|
||
den Kanten ablesen: Beim Hochscrollen schwebt sie
|
||
absichtlich ueber dem unteren Teil der Spalten. Die Frage
|
||
„liegt sie unter beiden?" war die von vorher -- sie hat nach
|
||
der Reparatur einen Fehler gemeldet, wo das Verhalten
|
||
richtig ist.
|
||
Was „unten" wirklich heisst, steht in der Reihenfolge: Sie
|
||
kommt im Dokument NACH beiden Spalten. Das gilt beim
|
||
Scrollen wie beim Stillstand. */
|
||
letzte: (() => {
|
||
const e = document.querySelector(".transport");
|
||
const r = document.querySelector(".regiekachel.kachel--sendung");
|
||
if (!e || !r) return false;
|
||
/* 4 = DOCUMENT_POSITION_FOLLOWING */
|
||
return !!(r.compareDocumentPosition(e) & 4);
|
||
})(),
|
||
klebt: (() => {
|
||
const e = document.querySelector(".transport");
|
||
return e ? getComputedStyle(e).position : "(?)";
|
||
})(),
|
||
fenster: window.innerHeight,
|
||
};
|
||
});
|
||
const p = platz;
|
||
console.log(` Regieplatz: Spalte ${p.laeuft?.l}..${p.laeuft?.r} · `
|
||
+ `Leiste ${p.leiste?.l}..${p.leiste?.r} bei y=${p.leiste?.o} (${p.leiste?.h} px hoch)`);
|
||
/* ==== VERDECKT DIE KLEBENDE LEISTE ETWAS? ==================
|
||
Sie schwebt ueber dem unteren Teil der Regie -- das ist
|
||
gewollt. Was NICHT gewollt ist: dass ein Knopf darunter
|
||
liegt und deshalb nicht mehr gedrueckt werden kann. Gemessen
|
||
wird, was der Browser selbst sagt: Liegt an der Mitte eines
|
||
Knopfes etwas anderes obenauf? */
|
||
const verdeckt = await host.seite.evaluate(() => {
|
||
const raus = [];
|
||
const leiste = document.querySelector(".transport");
|
||
if (!leiste) return raus;
|
||
for (const e of document.querySelectorAll("#pult-innen button, #pult-innen input,"
|
||
+ " #pult-innen select, #pult-innen label")) {
|
||
const r = e.getBoundingClientRect();
|
||
if (r.width === 0 || r.height === 0) continue;
|
||
const oben = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
|
||
if (!oben || e === oben || e.contains(oben)) continue;
|
||
if (!leiste.contains(oben)) continue; /* nur die Leiste zaehlt */
|
||
raus.push(`${e.id || e.className || e.tagName} (${Math.round(r.top)}..${Math.round(r.bottom)})`);
|
||
}
|
||
return raus;
|
||
});
|
||
if (verdeckt.length) {
|
||
console.log(` ACHTUNG: die Transportleiste liegt ueber ${verdeckt.length} Bedienelement(en): `
|
||
+ verdeckt.slice(0, 5).join(", "));
|
||
ausgang = 1;
|
||
} else {
|
||
console.log(" Die Transportleiste verdeckt kein Bedienelement der Regie.");
|
||
}
|
||
|
||
|
||
console.log(` Play-Knopf ${p.spiel?.b}x${p.spiel?.h} px · seitlich ueber ${p.ueber} px `
|
||
+ `· Leiste ohne Scrollen sichtbar: ${p.sichtbar} `
|
||
+ `(${p.klebt}, nach beiden Spalten: ${p.letzte})`);
|
||
if (!p.sichtbar) {
|
||
console.log(" ACHTUNG: Die Transportleiste ist nur mit Scrollen zu erreichen.");
|
||
ausgang = 1;
|
||
}
|
||
if (!p.laeuft || !p.sendung || !p.bereit || !p.leiste) {
|
||
console.log(` ACHTUNG: Es fehlt eine Kachel (laeuft=${!!p.laeuft}, `
|
||
+ `sendung=${!!p.sendung}, bereit=${!!p.bereit}, Leiste=${!!p.leiste}).`);
|
||
ausgang = 1;
|
||
} else {
|
||
/* UNTEREINANDER, NICHT NEBENEINANDER. In der schmalen
|
||
Regiespalte ist das die Absicht: Jede Kachel bekommt die
|
||
volle Breite, und nichts muss seitlich geschoben werden.
|
||
Geprueft wird deshalb, dass sie sich NICHT ueberlappen und
|
||
jede die Spalte fuellt -- zwei halbe Kacheln nebeneinander
|
||
waeren in 360 Pixeln unlesbar. */
|
||
const untereinander = p.sendung.o >= p.laeuft.u - 1
|
||
&& p.bereit.o >= p.sendung.u - 1;
|
||
console.log(` Kacheln (y): laeuft ${p.laeuft.o}..${p.laeuft.u} · sendung ${p.sendung.o}..${p.sendung.u}`
|
||
+ ` · bereit ${p.bereit.o}..${p.bereit.u} · alle ${p.laeuft.b} px breit`
|
||
+ ` · untereinander: ${untereinander}`);
|
||
if (!untereinander) {
|
||
console.log(" ACHTUNG: Die drei Kacheln stehen nicht untereinander.");
|
||
ausgang = 1;
|
||
}
|
||
/* DIE LEISTE KOMMT NACH ALLEM. `position: sticky` braucht sie
|
||
seit dem Umbau „Regie links" nicht mehr: Sie ist eine eigene
|
||
Zeile des Saals und steht damit von selbst unten. Was gilt,
|
||
ist die Reihenfolge -- und die gilt beim Scrollen wie beim
|
||
Stillstand. */
|
||
if (!p.letzte) {
|
||
console.log(` ACHTUNG: Die Leiste kommt nicht nach den Kacheln.`);
|
||
ausgang = 1;
|
||
}
|
||
/* Sie soll ueber die GANZE Breite des Saals gehen -- eine
|
||
Leiste, die nur so breit ist wie die Regiespalte, waere
|
||
keine Leiste unten, sondern ein Anhaengsel der Regie. */
|
||
if (p.leiste.b < p.laeuft.b * 2) {
|
||
console.log(` ACHTUNG: Die Leiste ist nur ${p.leiste.b} px breit `
|
||
+ `(die Regiespalte allein ist ${p.laeuft.b} px).`);
|
||
ausgang = 1;
|
||
}
|
||
/* Der Play-Knopf ist der einzige, den man im Live blind treffen
|
||
muss. 44 px ist die Hausgrenze; er soll deutlich darueber
|
||
liegen. */
|
||
if ((p.spiel?.b || 0) < 52 || (p.spiel?.h || 0) < 52) {
|
||
console.log(" ACHTUNG: Der Play-Knopf ist zu klein zum Blindtreffen.");
|
||
ausgang = 1;
|
||
}
|
||
if (p.ueber > 1) {
|
||
console.log(` ACHTUNG: Der Regieplatz laeuft ${p.ueber} px seitlich hinaus.`);
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* ---- Und der Knopf zum Teilen ---- */
|
||
const teilen = await host.seite.evaluate(() => {
|
||
const e = document.getElementById("s-teilen");
|
||
if (!e) return null;
|
||
const r = e.getBoundingClientRect();
|
||
return {
|
||
da: r.width > 0 && r.height > 0,
|
||
b: Math.round(r.width), h: Math.round(r.height),
|
||
wort: document.getElementById("s-teilen-wort")?.textContent || "",
|
||
gedrueckt: e.getAttribute("aria-pressed"),
|
||
};
|
||
});
|
||
console.log(` Bildschirm teilen: „${teilen?.wort}" ${teilen?.b}x${teilen?.h} px, `
|
||
+ `gedrueckt=${teilen?.gedrueckt}`);
|
||
if (!teilen?.da || teilen.h < 40 || teilen.wort !== "Bildschirm") {
|
||
console.log(" ACHTUNG: Der Knopf zum Teilen fehlt oder ist zu klein.");
|
||
ausgang = 1;
|
||
}
|
||
await bild(host.seite, "23-regieplatz");
|
||
}
|
||
|
||
const regie = await host.seite.evaluate(() => {
|
||
const t = (id) => document.getElementById(id)?.textContent?.trim() || "";
|
||
const sicht = (w) => {
|
||
const e = document.querySelector(w);
|
||
if (!e) return false;
|
||
const r = e.getBoundingClientRect();
|
||
return r.width > 0 && r.height > 0;
|
||
};
|
||
return {
|
||
auf: document.getElementById("pult")?.dataset.auf,
|
||
lampe: t("pult-wort"),
|
||
uhr: t("pult-zeit"),
|
||
dabei: t("z-dabei"),
|
||
sicht_zahl: t("z-sicht"),
|
||
last: t("z-last"),
|
||
spur: t("spur-titel"),
|
||
zeit: t("v-zeit"),
|
||
reiter: document.querySelectorAll("#reiter .reiter__knopf").length,
|
||
tafeln: document.querySelectorAll(".pult-tafel").length,
|
||
tafel_sichtbar: [...document.querySelectorAll(".pult-tafel")].filter((e) => !e.hidden).length,
|
||
regler: document.querySelectorAll("#regler .regler").length,
|
||
pegel: document.querySelectorAll("#regler .pegel").length,
|
||
knoepfe_sichtbar: ["p-vorbereiten", "p-starten", "p-beenden"]
|
||
.filter((id) => sicht("#" + id)).length,
|
||
/* WOHER KOMMT EIN UEBERLAUF? Die blosse Zahl „197 Pixel zum
|
||
Scrollen" schickt einen ins Raten. Die Hoehen daneben zeigen
|
||
in einer Zeile, welcher Kasten zu gross ist. */
|
||
hoehen: (() => {
|
||
const h = (w) => {
|
||
const e = document.querySelector(w);
|
||
return e ? Math.round(e.getBoundingClientRect().height) : -1;
|
||
};
|
||
return `fenster ${window.innerHeight} · saal ${h("#saal")}`
|
||
+ ` · kino ${h("#teil-live")} · pult ${h("#pult")}`
|
||
+ ` (leiste ${h(".pult__leiste")} + innen ${h("#pult-innen")})`
|
||
+ ` · tafeln sichtbar ${[...document.querySelectorAll(".pult-tafel")]
|
||
.filter((e) => e.getBoundingClientRect().height > 0).length}`
|
||
+ ` · innen rollt ${document.getElementById("pult-innen").scrollHeight}`;
|
||
})(),
|
||
kinder: [...document.getElementById("pult-innen").children].map((e) => {
|
||
const r = e.getBoundingClientRect();
|
||
const c = getComputedStyle(e);
|
||
return `${e.className || e.tagName}:${Math.round(r.top)}+${Math.round(r.height)}`
|
||
+ `/${c.display}/${c.position}`;
|
||
}).join(" "),
|
||
};
|
||
});
|
||
console.log(" " + regie.hoehen);
|
||
console.log(" " + regie.kinder);
|
||
console.log(` Lampe „${regie.lampe}" Uhr ${regie.uhr} `
|
||
+ `Sehen zu ${regie.dabei} mit Bild ${regie.sicht_zahl} Upload ${regie.last}`);
|
||
console.log(` Spur: „${regie.spur}" ${regie.zeit}`);
|
||
console.log(` ${regie.reiter} Register, ${regie.tafel_sichtbar} Tafel offen, `
|
||
+ `${regie.regler} Regler mit ${regie.pegel} Pegeln, `
|
||
+ `${regie.knoepfe_sichtbar}/3 grosse Knoepfe sichtbar`);
|
||
|
||
/* JEDE DIESER ZAHLEN KANN STILL FALSCH SEIN -- und genau dann sieht
|
||
ein Bildschirmfoto trotzdem gut aus. Deshalb werden sie geprueft
|
||
und nicht nur ausgegeben. */
|
||
if (regie.auf !== "ja") {
|
||
console.log(" ACHTUNG: Die Regie liess sich nicht aufklappen.");
|
||
ausgang = 1;
|
||
}
|
||
if (!/^\d+:\d\d/.test(regie.uhr) || regie.uhr === "0:00") {
|
||
console.log(` ACHTUNG: Die Laufzeit steht still (${regie.uhr}).`);
|
||
ausgang = 1;
|
||
}
|
||
if (!/kbit\/s|Mbit\/s/.test(regie.last)) {
|
||
console.log(` ACHTUNG: Der Upload wird nicht gemessen (${regie.last}).`);
|
||
ausgang = 1;
|
||
}
|
||
/* GEZAEHLT, NICHT FESTGESCHRIEBEN. Vorher stand hier „genau 5" --
|
||
und beim sechsten Register wurde die Messung rot, obwohl nichts
|
||
kaputt war. Was zaehlt, ist: zu jedem Reiter gehoert eine Tafel,
|
||
und genau eine ist offen. */
|
||
if (regie.reiter < 5 || regie.reiter !== regie.tafeln || regie.tafel_sichtbar !== 1) {
|
||
console.log(` ACHTUNG: Die Register stimmen nicht (${regie.reiter} Reiter, `
|
||
+ `${regie.tafeln} Tafeln, ${regie.tafel_sichtbar} offen).`);
|
||
ausgang = 1;
|
||
}
|
||
if (regie.regler !== 4 || regie.pegel !== 4 || regie.knoepfe_sichtbar !== 3) {
|
||
console.log(" ACHTUNG: Regler, Pegel oder die grossen Knoepfe fehlen.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ---- Kamera aus und Chat-Stufe ------------------------------------
|
||
Beides ist erst gebaut, wenn es beim ZUSCHAUER ankommt. */
|
||
console.log("");
|
||
console.log(" --- Kamera aus, Chat zu ---");
|
||
|
||
await host.seite.click("#s-kamera").catch(() => {});
|
||
await host.seite.waitForTimeout(1200);
|
||
const nachKamera = await zuschauer.seite.evaluate(() => {
|
||
const f = document.querySelector('#kameras .kamera[data-kamera="aus"]');
|
||
return {
|
||
schild: f ? f.querySelector(".kamera__aus")?.textContent?.trim() : null,
|
||
wessen: f ? f.querySelector(".kamera__name")?.textContent?.trim() : null,
|
||
videoVersteckt: f ? getComputedStyle(f.querySelector("video")).opacity === "0" : false,
|
||
};
|
||
});
|
||
console.log(` Zuschauerin sieht: „${nachKamera.schild || "(nichts)"}" `
|
||
+ `bei ${nachKamera.wessen || "(niemandem)"}, Bild verdeckt=${nachKamera.videoVersteckt}`);
|
||
if (nachKamera.schild !== "Kamera aus" || !nachKamera.videoVersteckt) {
|
||
console.log(" ACHTUNG: Das abgeschaltete Bild kommt nicht als Schild an.");
|
||
ausgang = 1;
|
||
}
|
||
await host.seite.click("#s-kamera").catch(() => {});
|
||
await host.seite.waitForTimeout(900);
|
||
|
||
await host.seite.click('#chat-stufen .chatstufe[data-modus="team"]').catch(() => {});
|
||
await host.seite.waitForTimeout(1200);
|
||
const nachChat = await zuschauer.seite.evaluate(() => {
|
||
const f = document.getElementById("chat-feld");
|
||
return { gesperrt: !!f?.disabled, grund: f?.placeholder || "",
|
||
stufe: document.getElementById("schiene")?.dataset.chat };
|
||
});
|
||
console.log(` Zuschauerin: Feld gesperrt=${nachChat.gesperrt} `
|
||
+ `Grund „${nachChat.grund}" Stufe=${nachChat.stufe}`);
|
||
/* GEGENPROBE AM SERVER. Ein ausgegrautes Feld haelt niemanden auf,
|
||
der die Schnittstelle kennt -- also wird auch dort nachgefragt. */
|
||
const trotzdem = await roh("/workspace/api/reaktion/chat", {
|
||
method: "POST", keks: k.gast, rumpf: { text: "geht das trotzdem?" },
|
||
});
|
||
console.log(` ... und der Server lehnt ab: ${trotzdem.code}/${trotzdem.j?.fehler}`);
|
||
if (!nachChat.gesperrt || !nachChat.grund.includes("Team") || trotzdem.code !== 403) {
|
||
console.log(" ACHTUNG: Die Chat-Stufe kommt nicht an.");
|
||
ausgang = 1;
|
||
}
|
||
await host.seite.click('#chat-stufen .chatstufe[data-modus="offen"]').catch(() => {});
|
||
await host.seite.waitForTimeout(900);
|
||
|
||
/* ---- Die Spenden -------------------------------------------------
|
||
Der ganze Weg: eintragen -> Karte laeuft bei allen. Melden ->
|
||
wartet. Bestaetigen -> laeuft. Und eine unbestaetigte Meldung
|
||
darf NICHT im Bild stehen. */
|
||
console.log("");
|
||
console.log(" --- Spenden ---");
|
||
|
||
/* Damit es Betragsknoepfe gibt, braucht die Unterstuetzen-Seite eine
|
||
PayPal.me-Adresse. Sie wird ueber den echten Weg gesetzt -- eine
|
||
Zeile direkt in die Datenbank zu schreiben wuerde genau die
|
||
Pruefungen umgehen, um die es geht. */
|
||
const wege = await roh("/workspace/api/unterstuetzung", { keks: k.admin });
|
||
const ppWeg = (wege.j?.wege || wege.j?.liste || []).find((w) => w.schluessel === "paypal");
|
||
if (ppWeg) {
|
||
await roh(`/workspace/api/unterstuetzung/${ppWeg.id}`, {
|
||
method: "PUT", keks: k.admin,
|
||
rumpf: {
|
||
ziel: "https://paypal.me/probehase", knopf: "Dogi was ausgeben", sichtbar: true,
|
||
},
|
||
});
|
||
}
|
||
const spLage = await roh("/workspace/api/spenden", { keks: k.gast });
|
||
console.log(` Betragsknoepfe: ${(spLage.j?.knoepfe || []).map((b) => b.ziel.split("/").pop()).join(", ") || "(keine)"}`);
|
||
if ((spLage.j?.knoepfe || []).length !== 4) {
|
||
console.log(" ACHTUNG: Die Betragsknoepfe fehlen.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* DIE ZUSCHAUERSEITE WIRD NEU GELADEN. Sie wurde geoeffnet, BEVOR
|
||
die PayPal-Adresse gesetzt war -- ohne sie gibt es keinen
|
||
Geben-Kasten, und der ist ab hier das Gemessene. Ein Neuladen ist
|
||
dabei nicht geschummelt, sondern der Normalfall: So kommt jeder
|
||
an, der spaeter dazustoesst. */
|
||
await zuschauer.seite.reload({ waitUntil: "domcontentloaded" });
|
||
await zuschauer.seite.waitForTimeout(1800);
|
||
|
||
/* 1. Von Hand eintragen -- die Karte muss bei der Zuschauerin laufen. */
|
||
await zuschauer.seite.evaluate(() => { window.__karten = []; });
|
||
await zuschauer.seite.evaluate(() => {
|
||
const t = document.getElementById("spendentafel");
|
||
new MutationObserver((m) => {
|
||
for (const e of m) {
|
||
for (const n of e.addedNodes) {
|
||
if (n.querySelector) {
|
||
window.__karten.push({
|
||
/* NUR DIE ZAHL, NICHT DIE GANZE ZEILE. Seit dem
|
||
28.09. steht neben ihr ein Zeichen und das Wort
|
||
„Dogen"; `textContent` klebte beides zusammen
|
||
(„2,5Dogen"), und eine Suche mit regulaerem Ausdruck
|
||
darin traefe frueher oder spaeter das Falsche. */
|
||
betrag: n.querySelector(".dogen__zahl")?.textContent?.trim(),
|
||
wort: n.querySelector(".dogen__wort")?.textContent?.trim(),
|
||
wer: n.querySelector(".spendenkarte__wer")?.textContent?.trim(),
|
||
stufe: n.querySelector(".spendenkarte__stufe")?.textContent?.trim(),
|
||
gruss: n.querySelector(".spendenkarte__gruss")?.textContent?.trim(),
|
||
/* Welche Muenze gross auf der Karte steht -- und ob die
|
||
kleine daneben weggenommen wurde. Dieselbe Muenze in
|
||
zwei Groessen auf einer Karte saehe aus wie ein
|
||
Versehen. */
|
||
muenze: (n.querySelector(".spendenkarte__bild img")
|
||
?.getAttribute("src") || "").split("/").pop(),
|
||
kleinWeg: n.dataset?.muenze === "ja",
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}).observe(t, { childList: true });
|
||
});
|
||
|
||
await roh("/workspace/api/spenden", {
|
||
method: "POST", keks: k.admin,
|
||
rumpf: { dogen_mille: 2500, name: "Nachtfalter", gruss: "Fuer das Rudel!" },
|
||
});
|
||
await zuschauer.seite.waitForFunction(
|
||
() => document.querySelector('.spendenkarte[data-da="ja"]'), null, { timeout: 8000 },
|
||
).catch(() => {});
|
||
await zuschauer.seite.waitForTimeout(700);
|
||
await bild(zuschauer.seite, "10-spendenkarte");
|
||
|
||
/* ==== 2. DER ZUSCHAUER GIBT UND SCHREIBT SELBST ==================
|
||
|
||
Filipe: „der text der dazu erscheint sollen die leute selber
|
||
schreiben koennen ... es sollen personalisierte texte sein von den
|
||
leuten selbst."
|
||
|
||
Gemessen wird der WEG DURCH DIE OBERFLAECHE und nicht der Aufruf
|
||
dahinter: Ein Knopf, den man drueckt, ein Feld, das man ausfuellt,
|
||
ein Text, der ankommt. Der Aufruf allein haette nie gezeigt, ob
|
||
das Feld ueberhaupt existiert.
|
||
|
||
Und dieselbe Meldung beantwortet die alte Frage gleich mit: Sie
|
||
ist UNBESTAETIGT und darf deshalb NICHT im Bild stehen. */
|
||
const geben = await zuschauer.seite.evaluate(() => {
|
||
const k2 = document.getElementById("geben");
|
||
const knoepfe = [...document.querySelectorAll(".geben__betrag")];
|
||
return {
|
||
da: k2 && !k2.hidden,
|
||
knoepfe: knoepfe.length,
|
||
zahlen: knoepfe.map((a) => a.querySelector(".dogen__zahl")?.textContent || "?"),
|
||
/* ==== DAS ZEICHEN MUSS DA SEIN -- UND GELADEN ==============
|
||
Seit dem 28.09. ist es ein BILD (Filipes Muenzen) und keine
|
||
Zeichnung mehr. Gezaehlt wird deshalb nicht, ob ein Knoten da
|
||
ist, sondern ob er etwas ZEIGT: Ein `img`, das 404 antwortet,
|
||
steht genauso im Dokument wie eines, das laedt -- und auf dem
|
||
Bildschirm ist an seiner Stelle nichts.
|
||
`naturalWidth > 0` ist der Unterschied, und er ist genau der,
|
||
den man sonst erst im Livestream sieht. */
|
||
zeichen: knoepfe.filter((a) => {
|
||
const b = a.querySelector(".dogen__zeichen");
|
||
if (!b) return false;
|
||
return b.tagName.toLowerCase() === "img" ? b.naturalWidth > 0 : true;
|
||
}).length,
|
||
/* ==== HIER MUSS DER PREIS STEHEN ==========================
|
||
Filipe: „die leute sollen nur sehen was dogen kosten. sonst
|
||
wissen die ja nicht wie viel es kosten wird."
|
||
Hier stand bis zum 28.09. das Gegenteil -- „nirgends ein
|
||
Euro". Das war fuer jede ANZEIGE richtig und fuer den einen
|
||
Kaufknopf falsch. */
|
||
preise: knoepfe.map((a) => a.querySelector(".geben__preis")?.textContent?.trim() || ""),
|
||
/* ==== UND SONST NIRGENDS AUF DER SEITE ====================
|
||
Die staerkste Form der Frage: den ganzen sichtbaren Text
|
||
nehmen, den Geben-Kasten herausschneiden und nachsehen, ob
|
||
irgendwo noch ein Eurozeichen uebrig ist. Eine Pruefung, die
|
||
nur den Kasten ansieht, uebersieht genau die Stelle, an der
|
||
es versehentlich landet. */
|
||
geldSonstwo: (() => {
|
||
const alles = document.body.textContent || "";
|
||
const kasten = k2?.textContent || "";
|
||
return /€|EUR/.test(alles.split(kasten).join(" "));
|
||
})(),
|
||
};
|
||
});
|
||
console.log(` Geben-Kasten: ${geben.knoepfe} Knoepfe (${geben.zahlen.join(" · ")}), `
|
||
+ `${geben.zeichen} mit Zeichen`);
|
||
console.log(` Preise am Knopf: ${geben.preise.join(" · ")} `
|
||
+ `| Geld sonstwo auf der Seite: ${geben.geldSonstwo}`);
|
||
if (!geben.da || geben.knoepfe !== 4 || geben.zeichen !== 4
|
||
|| geben.zahlen[0] !== "0,2") {
|
||
console.log(" ACHTUNG: Der Geben-Kasten zeigt nicht, was er soll.");
|
||
ausgang = 1;
|
||
}
|
||
/* WER BEZAHLT, MUSS VORHER WISSEN, WAS ER ZAHLT. Vier Knoepfe, vier
|
||
Preise -- ein Knopf ohne Preis waere der eine, bei dem es niemand
|
||
merkt. */
|
||
if (geben.preise.some((p) => !/\d/.test(p) || !p.includes("€"))) {
|
||
console.log(` ACHTUNG: An einem Knopf fehlt der Preis (${geben.preise.join(" | ")}).`);
|
||
ausgang = 1;
|
||
}
|
||
/* UND SONST NIRGENDS. Ein Zuschauer sieht den Preis dort, wo er
|
||
kauft -- und sonst nur Dogen. */
|
||
if (geben.geldSonstwo) {
|
||
console.log(" ACHTUNG: Auf der Zuschauerseite steht Geld ausserhalb der Kaufknoepfe.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ==== DIE PROBE AENDERT NUR DAS, WAS SIE AENDERN MUSS ============
|
||
Der Knopf ist ein Verweis mit `target="_blank"` und fuehrt zu
|
||
PayPal. Ein Klick in der Messung wuerde also ins Netz gehen. Ein
|
||
Zuhoerer in der EINFANGPHASE nimmt nur die Navigation weg -- der
|
||
Zuhoerer der Seite laeuft danach ganz normal.
|
||
|
||
Das ist die Lehre vom 28.09. in klein: Eine Probe, die mehr
|
||
aendert als das, wonach man sucht, beantwortet eine andere
|
||
Frage. Hier wird kein Ereignis nachgebaut und keine Funktion
|
||
direkt gerufen -- es wird wirklich geklickt. */
|
||
await zuschauer.seite.evaluate(() => {
|
||
document.addEventListener("click", (e) => {
|
||
if (e.target.closest && e.target.closest("a.geben__betrag")) e.preventDefault();
|
||
}, true);
|
||
});
|
||
await zuschauer.seite.click(".geben__betrag");
|
||
await zuschauer.seite.waitForTimeout(400);
|
||
|
||
const felder = await zuschauer.seite.evaluate(() => {
|
||
const name = document.getElementById("geben-name");
|
||
const text = document.getElementById("geben-text");
|
||
return {
|
||
offen: !document.getElementById("geben-nach").hidden,
|
||
frage: document.getElementById("geben-frage")?.textContent?.trim() || "",
|
||
knopf: document.getElementById("geben-melden")?.textContent?.trim() || "",
|
||
name: name?.value || "",
|
||
grenze: text?.maxLength || 0,
|
||
/* Am Finger muessen beide Felder gross genug sein. */
|
||
hoch: Math.round(text?.getBoundingClientRect().height || 0),
|
||
};
|
||
});
|
||
console.log(` Nach dem Tippen: „${felder.frage}" · Knopf „${felder.knopf}"`);
|
||
console.log(` Felder: Name „${felder.name}", Text bis ${felder.grenze} Zeichen, `
|
||
+ `${felder.hoch} px hoch`);
|
||
if (!felder.offen || felder.grenze !== 140 || felder.name !== "Nachtfalter"
|
||
|| /€|EUR/.test(felder.frage) || !felder.frage.includes("Dogen")) {
|
||
console.log(" ACHTUNG: Die Felder fuer den eigenen Text stimmen nicht.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* Der Zaehler steht erst da, wenn es eng wird -- vorher waere er
|
||
eine Aufsicht statt einer Hilfe. */
|
||
const kurz = await zuschauer.seite.evaluate(() => {
|
||
const t = document.getElementById("geben-text");
|
||
t.value = "Kurz";
|
||
t.dispatchEvent(new Event("input", { bubbles: true }));
|
||
return document.getElementById("geben-rest")?.textContent || "";
|
||
});
|
||
const lang = await zuschauer.seite.evaluate(() => {
|
||
const t = document.getElementById("geben-text");
|
||
t.value = "Danke fuer den Abend, ihr Lieben! Bleibt genau so wie ihr seid "
|
||
+ "-- das hier ist das Beste am ganzen Tag, ehrlich jetzt.";
|
||
t.dispatchEvent(new Event("input", { bubbles: true }));
|
||
return document.getElementById("geben-rest")?.textContent || "";
|
||
});
|
||
console.log(` Zaehler: bei „Kurz" „${kurz}", bei langem Text „${lang}"`);
|
||
if (kurz !== "" || !lang.startsWith("noch ")) {
|
||
console.log(" ACHTUNG: Der Zaehler erscheint zur falschen Zeit.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
await zuschauer.seite.click("#geben-melden");
|
||
await zuschauer.seite.waitForTimeout(1400);
|
||
const danach = await zuschauer.seite.evaluate(() => ({
|
||
danke: document.getElementById("geben-danke")?.textContent?.trim() || "",
|
||
offen: !document.getElementById("geben-nach").hidden,
|
||
leer: document.getElementById("geben-text")?.value === "",
|
||
}));
|
||
console.log(` Danach: „${danach.danke}"`);
|
||
if (danach.offen || !danach.leer || !danach.danke.includes("Text")) {
|
||
console.log(" ACHTUNG: Nach dem Melden stimmt die Antwort nicht.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
const nachMelden = await zuschauer.seite.evaluate(() => window.__karten);
|
||
console.log(` Karten bei der Zuschauerin: ${JSON.stringify(nachMelden)}`);
|
||
if (nachMelden.length !== 1 || nachMelden[0]?.betrag !== "2,5") {
|
||
console.log(" ACHTUNG: Die eingetragene Spende kommt nicht als Karte an.");
|
||
ausgang = 1;
|
||
}
|
||
if (nachMelden.some((k2) => k2.betrag === "0,2")) {
|
||
console.log(" ACHTUNG: Eine UNBESTAETIGTE Meldung stand im Bild.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* 3. Bestaetigen -- jetzt muss sie laufen. */
|
||
const offen = await roh("/workspace/api/spenden", { keks: k.admin });
|
||
const wartet = (offen.j?.offen || [])[0];
|
||
console.log(` Im Pult wartet: ${wartet ? wartet.dogen_mille / 1000 + " Dogen von " + wartet.name : "(nichts)"}`);
|
||
if (!wartet) {
|
||
console.log(" ACHTUNG: Die Meldung kommt nicht im Pult an.");
|
||
ausgang = 1;
|
||
} else {
|
||
/* ==== DIE SCHRANKE VOR DEM STREAM ============================
|
||
Der Text kommt von einem Fremden und steht gleich im Bild.
|
||
Geprueft wird deshalb, dass er (a) im Pult ANKOMMT, (b) sich
|
||
dort AENDERN laesst und (c) genau so auf der Karte landet. */
|
||
console.log(` Der Zuschauer schrieb: „${(wartet.gruss || "").slice(0, 60)}…"`);
|
||
if (!wartet.gruss || !wartet.gruss.startsWith("Danke fuer den Abend")) {
|
||
console.log(" ACHTUNG: Der selbst geschriebene Text kommt nicht im Pult an.");
|
||
ausgang = 1;
|
||
}
|
||
const inFeldern = await host.seite.evaluate(() => {
|
||
const z = document.querySelector("#sp-offen .spendenzeile");
|
||
const f = z ? [...z.querySelectorAll(".spendenzeile__feld")] : [];
|
||
return {
|
||
felder: f.length,
|
||
wert: f[1]?.value || "",
|
||
grenze: f[1]?.maxLength || 0,
|
||
zahl: z?.querySelector(".dogen__zahl")?.textContent || "",
|
||
};
|
||
});
|
||
console.log(` Im Pult: ${inFeldern.felder} Felder, Text bis ${inFeldern.grenze} `
|
||
+ `Zeichen, Zahl „${inFeldern.zahl}"`);
|
||
if (inFeldern.felder !== 2 || inFeldern.grenze !== 140
|
||
|| inFeldern.zahl !== "0,2") {
|
||
console.log(" ACHTUNG: Die Leitung kann den Text nicht sehen oder nicht aendern.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
await roh(`/workspace/api/spenden/${wartet.id}/bestaetigen`, {
|
||
method: "POST", keks: k.admin,
|
||
rumpf: { dogen_mille: 1000, gruss: "Danke fuer den Abend!" },
|
||
});
|
||
/* AUF DAS MERKMAL WARTEN, NICHT AUF DIE UHR. Die erste Karte
|
||
laeuft elf Sekunden (die hoechste Stufe steht am laengsten); die
|
||
zweite wartet in der Schlange, damit sie nicht uebereinander
|
||
liegen. Eine feste Pause haette das als Fehler gemeldet. */
|
||
await zuschauer.seite.waitForFunction(
|
||
() => (window.__karten || []).length >= 2, null, { timeout: 20000 },
|
||
).catch(() => {});
|
||
const nachJa = await zuschauer.seite.evaluate(() => window.__karten);
|
||
console.log(` Nach dem Bestaetigen: ${JSON.stringify(nachJa.slice(-1))}`);
|
||
if (nachJa.length !== 2 || nachJa[1]?.betrag !== "1") {
|
||
console.log(" ACHTUNG: Die bestaetigte Spende laeuft nicht -- oder mit falschem Betrag.");
|
||
ausgang = 1;
|
||
}
|
||
/* DIE STUFE MUSS ZUR ZAHL PASSEN. 2,5 Dogen ist die hoechste
|
||
(„Rudel-Legende"), 1 Doge die mittlere („Starke Runde"). Ohne
|
||
diese Probe koennte jede Karte dieselbe Stufe tragen und es
|
||
faellt niemandem auf. */
|
||
console.log(` Stufen: ${nachMelden[0]?.stufe} / ${nachJa[1]?.stufe}`);
|
||
/* EINZAHL UND MEHRZAHL. „1 Dogen" waere die Sorte Fehler, die
|
||
jeder sieht und niemand meldet. */
|
||
console.log(` Einheit: „${nachMelden[0]?.wort}" bei 2,5 · `
|
||
+ `„${nachJa[1]?.wort}" bei 1`);
|
||
if (nachMelden[0]?.wort !== "Dogen" || nachJa[1]?.wort !== "Doge") {
|
||
console.log(" ACHTUNG: Einzahl und Mehrzahl stimmen nicht.");
|
||
ausgang = 1;
|
||
}
|
||
/* Der Text, nicht das Bild: Grossbuchstaben macht das Stilblatt. */
|
||
if (nachMelden[0]?.stufe !== "Rudel-Legende" || nachJa[1]?.stufe !== "Starke Runde") {
|
||
console.log(" ACHTUNG: Die Stufe passt nicht zur Zahl.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ==== DER BERICHTIGTE TEXT STEHT AUF DER KARTE ================
|
||
Nicht der, den der Zuschauer geschrieben hat, sondern der, den
|
||
die Leitung freigegeben hat. Andersherum waere die ganze
|
||
Schranke wirkungslos -- und niemandem fiele es auf, bis es
|
||
einmal zaehlt. */
|
||
console.log(` Auf der Karte steht: „${nachJa[1]?.gruss}"`);
|
||
if (nachJa[1]?.gruss !== "Danke fuer den Abend!") {
|
||
console.log(" ACHTUNG: Der freigegebene Text steht nicht auf der Karte.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ==== UND DIE PUNKTE SIND SOFORT DA ===========================
|
||
Filipe: „ich will dass die den leuten auch als punkte
|
||
hinzugefuegt werden."
|
||
|
||
GEMESSEN BEIM ZUSCHAUER UND OHNE NEULADEN. Genau hier war ein
|
||
Loch: `spendenHolen()` ruft bei einer Zuschauerin niemand auf,
|
||
das tut nur der Host -- ihr Stand waere erst beim naechsten
|
||
Seitenaufruf gewachsen. „Hinzugefuegt werden" heisst sichtbar
|
||
hinzugefuegt. */
|
||
await zuschauer.seite.waitForFunction(
|
||
() => !document.getElementById("geben-stand")?.hidden, null, { timeout: 8000 },
|
||
).catch(() => {});
|
||
const stand = await zuschauer.seite.evaluate(() => {
|
||
const p = document.getElementById("geben-stand");
|
||
return {
|
||
da: !!p && !p.hidden,
|
||
zahl: p?.querySelector(".dogen__zahl")?.textContent || "",
|
||
wort: p?.querySelector(".dogen__wort")?.textContent || "",
|
||
zeichen: (() => {
|
||
const b = p?.querySelector(".dogen__zeichen");
|
||
if (!b) return false;
|
||
return b.tagName.toLowerCase() === "img" ? b.naturalWidth > 0 : true;
|
||
})(),
|
||
text: p?.textContent?.trim() || "",
|
||
};
|
||
});
|
||
console.log(` Eigener Stand ohne Neuladen: „${stand.text}" `
|
||
+ `(Zeichen: ${stand.zeichen})`);
|
||
if (!stand.da || stand.zahl !== "1" || stand.wort !== "Doge" || !stand.zeichen) {
|
||
console.log(" ACHTUNG: Der eigene Dogen-Stand kommt nicht an.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ==== DIE MUENZE MUSS ZUR HOEHE DER SPENDE PASSEN =============
|
||
|
||
Filipe: „drei varianten fuer niedrige spenden. mittlere und
|
||
hohe spenden. aber pass sie sofort diesen 3 kategorien an."
|
||
|
||
Die erste Karte sind 2,5 Dogen (Rudel-Legende, oberes Drittel),
|
||
die zweite 1 Doge (Starke Runde, mittleres). Stuende auf beiden
|
||
dieselbe Muenze, waere die ganze Einteilung wirkungslos -- und
|
||
das sieht man einer einzelnen Karte nie an. */
|
||
console.log(` Muenzen: „${nachMelden[0]?.muenze}" bei 2,5 · `
|
||
+ `„${nachJa[1]?.muenze}" bei 1`);
|
||
console.log(` Kleine Muenze daneben weggenommen: `
|
||
+ `${nachMelden[0]?.kleinWeg} / ${nachJa[1]?.kleinWeg}`);
|
||
if (!/-hoch\.webp$/.test(nachMelden[0]?.muenze || "")
|
||
|| !/-mittel\.webp$/.test(nachJa[1]?.muenze || "")) {
|
||
console.log(" ACHTUNG: Die Muenze passt nicht zur Hoehe der Spende.");
|
||
ausgang = 1;
|
||
}
|
||
if (!nachMelden[0]?.kleinWeg || !nachJa[1]?.kleinWeg) {
|
||
console.log(" ACHTUNG: Die Muenze steht zweimal auf derselben Karte.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ---- UND DER SATZ LAESST SICH WECHSELN ----
|
||
Gemessen ueber die Probe beim Host: Sie schreibt nichts in die
|
||
Datenbank und ist damit der einzige Weg, eine Karte zu sehen,
|
||
ohne eine Spende zu erfinden. */
|
||
await roh("/workspace/api/spenden/zeichen/satz", {
|
||
method: "PUT", keks: k.admin, rumpf: { satz: "neon" },
|
||
});
|
||
await host.seite.waitForTimeout(700);
|
||
await roh("/workspace/api/spenden/probe", {
|
||
method: "POST", keks: k.admin, rumpf: { dogen_mille: 2500 },
|
||
});
|
||
/* AUF DIE PROBEKARTE WARTEN, NICHT AUF IRGENDEINE.
|
||
|
||
Beim ersten Lauf stand noch die vorige Karte („Starke Runde",
|
||
acht Sekunden) auf dem Bildschirm, und die Messung las deren
|
||
Muenze -- sie meldete „klassik-mittel" und damit einen Fehler,
|
||
den es nicht gab. Die Karten laufen in einer SCHLANGE: Die
|
||
naechste kommt erst, wenn die vorige abgelaufen ist. Also auf
|
||
das Merkmal warten, das nur die neue hat. */
|
||
await host.seite.waitForFunction(
|
||
() => [...document.querySelectorAll('.spendenkarte[data-da="ja"]')].some(
|
||
(k2) => k2.querySelector(".spendenkarte__wer")?.textContent === "Probe"
|
||
&& k2.querySelector(".spendenkarte__bild img")),
|
||
null, { timeout: 20000 },
|
||
).catch(() => {});
|
||
const nachWechsel = await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll('.spendenkarte[data-da="ja"]')]
|
||
.find((x) => x.querySelector(".spendenkarte__wer")?.textContent === "Probe");
|
||
const b = k2?.querySelector(".spendenkarte__bild img");
|
||
/* Und auch hier: geladen, nicht nur vorhanden. */
|
||
return b && b.naturalWidth > 0
|
||
? (b.getAttribute("src") || "").split("/").pop() : "(nicht geladen)";
|
||
});
|
||
console.log(` Nach dem Wechsel auf „Neon": „${nachWechsel}"`);
|
||
if (nachWechsel !== "neon-hoch.webp") {
|
||
console.log(" ACHTUNG: Der gewechselte Muenzsatz kommt nicht an.");
|
||
ausgang = 1;
|
||
}
|
||
await roh("/workspace/api/spenden/zeichen/satz", {
|
||
method: "PUT", keks: k.admin, rumpf: { satz: "klassik" },
|
||
});
|
||
}
|
||
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="spenden"]').catch(() => {});
|
||
await host.seite.waitForTimeout(700);
|
||
await bild(host.seite, "9-live-regie-spenden");
|
||
|
||
await vermessen(host.seite, "Host am Rechner");
|
||
await bild(host.seite, "3-live-host");
|
||
|
||
/* Und einmal die Warteschlange -- das ist der Teil, den Filipe
|
||
vorher benutzt, nicht waehrend der Sendung. */
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="liste"]').catch(() => {});
|
||
await host.seite.waitForTimeout(600);
|
||
const warte = await host.seite.evaluate(() => ({
|
||
zeilen: document.querySelectorAll("#warteliste .warte-zeile").length,
|
||
bilder: [...document.querySelectorAll(".warte-zeile__bild")]
|
||
.filter((b) => b.naturalWidth > 0).length,
|
||
titel: [...document.querySelectorAll(".warte-zeile__titel")]
|
||
.map((e) => e.textContent.trim()).join(" | "),
|
||
naechstes: document.getElementById("v-naechstes")?.disabled,
|
||
}));
|
||
console.log(` Warteschlange: ${warte.zeilen} Zeilen, `
|
||
+ `${warte.bilder} Vorschaubilder geladen, „Naechstes" `
|
||
+ `${warte.naechstes ? "grau" : "nutzbar"}`);
|
||
console.log(` ${warte.titel}`);
|
||
if (warte.zeilen !== 2 || warte.naechstes) {
|
||
console.log(" ACHTUNG: Die Warteschlange kommt nicht an.");
|
||
ausgang = 1;
|
||
}
|
||
await bild(host.seite, "7-live-regie-liste");
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="ton"]').catch(() => {});
|
||
await host.seite.waitForTimeout(500);
|
||
await bild(host.seite, "8-live-regie-ton");
|
||
if (host.konsole.length) console.log(" KONSOLE:", host.konsole.slice(0, 4).join(" | "));
|
||
|
||
await vermessen(zuschauer.seite, "Zuschauerin am Rechner");
|
||
await bild(zuschauer.seite, "4-live-zuschauer");
|
||
if (zuschauer.konsole.length) console.log(" KONSOLE:", zuschauer.konsole.slice(0, 4).join(" | "));
|
||
|
||
/* ---- Rollen und Moderation im Chat --------------------------------
|
||
|
||
Filipe: „die modis, linke hand und rechte hand soll im chat extra
|
||
aussehen und auch sachen wie stummschaltungen, sperrungen,
|
||
meldunge ... machen koennen."
|
||
|
||
GEMESSEN UND NICHT HERGELEITET. Die Schiene ist die schmalste
|
||
Flaeche der ganzen Seite, und in jede Beitragszeile ist gerade ein
|
||
Abzeichen UND ein zweiter Knopf dazugekommen. Genau so ist am
|
||
28.09. die Regie 197 px uebergelaufen: Die Rechnung ging auf, die
|
||
Wirklichkeit nicht. */
|
||
/* ---- Groesse und Ecke der Kamerafenster -------------------------
|
||
|
||
Filipe: „und danach perfektionierst du auch die verschiedenen
|
||
groessen von kamera und so."
|
||
|
||
GEMESSEN WIRD BEIM ZUSCHAUER. Dass in der Datenbank 1,5 steht,
|
||
sagt nichts darueber, ob das Fenster bei den anderen groesser
|
||
wird -- dazwischen liegen ein Rundruf, eine Variable im Stilblatt
|
||
und eine Rechnung mit `clamp`. */
|
||
console.log("");
|
||
console.log(" --- Kameragroesse und Ecke ---");
|
||
{
|
||
/* Sauberer Anfang: „Video gross", damit die Ecke ueberhaupt gilt. */
|
||
await roh("/workspace/api/reaktion/layout", {
|
||
method: "POST", keks: k.admin, rumpf: { layout: "kino" },
|
||
});
|
||
await zuschauer.seite.waitForTimeout(900);
|
||
|
||
const messen = () => zuschauer.seite.evaluate(() => {
|
||
const kino = document.getElementById("teil-live");
|
||
const kk = document.querySelector(".kameras");
|
||
const ein = document.querySelector(".kamera");
|
||
const lein = document.querySelector(".leinwand");
|
||
if (!kk || !ein || !lein) return null;
|
||
const r = ein.getBoundingClientRect();
|
||
const lr = lein.getBoundingClientRect();
|
||
const fenster = [...document.querySelectorAll(".kamera")];
|
||
return {
|
||
gross: kino.style.getPropertyValue("--kam-gross") || "(?)",
|
||
ecke: kino.dataset.ecke || "(?)",
|
||
breit: Math.round(r.width),
|
||
/* Wo sitzt der Stapel? Als Abstand zu den vier Kanten der
|
||
Leinwand -- daran erkennt man die Ecke ohne Vorwissen. */
|
||
links: Math.round(kk.getBoundingClientRect().left - lr.left),
|
||
rechts: Math.round(lr.right - kk.getBoundingClientRect().right),
|
||
oben: Math.round(kk.getBoundingClientRect().top - lr.top),
|
||
unten: Math.round(lr.bottom - kk.getBoundingClientRect().bottom),
|
||
/* Und liegt noch alles IN der Leinwand? Sie hat
|
||
`overflow: hidden` -- was daneben liegt, ist unsichtbar,
|
||
nicht abgeschnitten sichtbar. */
|
||
drin: fenster.filter((f) => {
|
||
const q = f.getBoundingClientRect();
|
||
return q.left >= lr.left - 1 && q.right <= lr.right + 1
|
||
&& q.top >= lr.top - 1 && q.bottom <= lr.bottom + 1;
|
||
}).length,
|
||
fenster: fenster.length,
|
||
leinwand: Math.round(lr.width),
|
||
};
|
||
});
|
||
|
||
const normal = await messen();
|
||
console.log(` 1×: Fenster ${normal?.breit} px, Ecke ${normal?.ecke}, `
|
||
+ `Abstände l/r/o/u ${normal?.links}/${normal?.rechts}/${normal?.oben}/${normal?.unten}, `
|
||
+ `${normal?.drin}/${normal?.fenster} im Bild`);
|
||
|
||
/* ---- DIE GROESSE MUSS WIRKEN ---- */
|
||
const { KAMERA_GROESSEN } = await import("./reaktion-tabellen.js");
|
||
const gross = KAMERA_GROESSEN[KAMERA_GROESSEN.length - 1];
|
||
await roh("/workspace/api/reaktion/layout", {
|
||
method: "POST", keks: k.admin, rumpf: { kamera_gross: gross },
|
||
});
|
||
await zuschauer.seite.waitForTimeout(900);
|
||
const dick = await messen();
|
||
console.log(` ${gross}×: Fenster ${dick?.breit} px, `
|
||
+ `${dick?.drin}/${dick?.fenster} im Bild (Leinwand ${dick?.leinwand} px)`);
|
||
|
||
if (!normal || !dick) {
|
||
console.log(" ACHTUNG: Die Kamerafenster liessen sich nicht messen.");
|
||
ausgang = 1;
|
||
} else {
|
||
/* NICHT AUF DEN FAKTOR GENAU. `clamp` deckelt die Grundbreite,
|
||
und `max-width: 46%` deckelt den Stapel -- beides ist Absicht.
|
||
Geprueft wird, dass es SPUERBAR waechst und im Bild bleibt. */
|
||
const wuchs = dick.breit / normal.breit;
|
||
console.log(` Das Fenster wuchs um das ${wuchs.toFixed(2)}-fache`);
|
||
if (wuchs < 1.3) {
|
||
console.log(" ACHTUNG: Die Groesse wirkt nicht.");
|
||
ausgang = 1;
|
||
}
|
||
if (dick.drin !== dick.fenster) {
|
||
console.log(" ACHTUNG: Ein Fenster liegt ausserhalb der Leinwand.");
|
||
ausgang = 1;
|
||
}
|
||
if (dick.breit > dick.leinwand * 0.5) {
|
||
console.log(" ACHTUNG: Ein Fenster deckt mehr als die halbe Leinwand zu.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* ---- DIE VIER ECKEN ----
|
||
Geprueft wird der ABSTAND zu den Kanten, nicht der Name der
|
||
Ecke. Eine Einstellung, die nur ihren eigenen Namen bestaetigt,
|
||
bestaetigt nichts. */
|
||
await roh("/workspace/api/reaktion/layout", {
|
||
method: "POST", keks: k.admin, rumpf: { kamera_gross: 1 },
|
||
});
|
||
const erwartet = {
|
||
ol: ["links", "oben"], or: ["rechts", "oben"],
|
||
ul: ["links", "unten"], ur: ["rechts", "unten"],
|
||
};
|
||
for (const [ecke, [waag, senk]] of Object.entries(erwartet)) {
|
||
await roh("/workspace/api/reaktion/layout", {
|
||
method: "POST", keks: k.admin, rumpf: { kamera_ecke: ecke },
|
||
});
|
||
await zuschauer.seite.waitForTimeout(800);
|
||
const m = await messen();
|
||
const nahWaag = m[waag] < (waag === "links" ? m.rechts : m.links);
|
||
const nahSenk = m[senk] < (senk === "oben" ? m.unten : m.oben);
|
||
console.log(` Ecke ${ecke}: l/r/o/u ${m.links}/${m.rechts}/${m.oben}/${m.unten} `
|
||
+ `-> ${waag}+${senk} ${nahWaag && nahSenk ? "stimmt" : "STIMMT NICHT"}`);
|
||
if (!nahWaag || !nahSenk || m.ecke !== ecke) {
|
||
console.log(` ACHTUNG: Die Ecke „${ecke}" kommt nicht an.`);
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
await bild(zuschauer.seite, "22-kamera-ecke");
|
||
|
||
/* ---- EIN KNOPF, DER NICHTS TUN KANN, IST GRAU ----
|
||
Die Ecke wirkt nur bei „Video gross"; die Groesse wirkt ueberall,
|
||
wo es Kamerafenster GIBT -- bei „Nur Video" gibt es keine.
|
||
|
||
GEMESSEN WIRD DER ZUSTAND DER KNOEPFE, nicht der Satz darunter.
|
||
Ein Hinweis, der die Wahrheit sagt, waehrend der Knopf daneben
|
||
bedienbar bleibt, ist die schlechtere Haelfte: Man liest ihn
|
||
nicht und drueckt trotzdem. */
|
||
const knoepfe = () => host.seite.evaluate(() => {
|
||
const e = [...document.querySelectorAll("#kamecke .kamecke__knopf")];
|
||
const g = [...document.querySelectorAll("#kamgroesse .kamgroesse__knopf")];
|
||
return {
|
||
ecken: e.length, eckenGrau: e.filter((x) => x.disabled).length,
|
||
groessen: g.length, groessenGrau: g.filter((x) => x.disabled).length,
|
||
hinweis: document.getElementById("kamecke-hinweis")?.textContent || "",
|
||
};
|
||
});
|
||
|
||
for (const [anordnung, sollEcken, sollGroessen, wort] of [
|
||
["kamera", "alle", "keine", "Mitte"],
|
||
["nur_video", "alle", "alle", "Nur Video"],
|
||
["kino", "keine", "keine", "Ecke"],
|
||
]) {
|
||
await roh("/workspace/api/reaktion/layout", {
|
||
method: "POST", keks: k.admin, rumpf: { layout: anordnung },
|
||
});
|
||
await host.seite.waitForTimeout(800);
|
||
const m = await knoepfe();
|
||
console.log(` „${anordnung}": ${m.eckenGrau}/${m.ecken} Ecken grau, `
|
||
+ `${m.groessenGrau}/${m.groessen} Größen grau`);
|
||
console.log(` Hinweis: „${m.hinweis.replace(/\s+/g, " ").trim()}"`);
|
||
const soll = (art, wieviel, da) => (wieviel === "alle" ? art === da : art === 0);
|
||
if (m.ecken !== 4 || m.groessen < 4) {
|
||
console.log(" ACHTUNG: Die Knoepfe fuer Groesse oder Ecke fehlen.");
|
||
ausgang = 1;
|
||
} else if (!soll(m.eckenGrau, sollEcken, m.ecken)
|
||
|| !soll(m.groessenGrau, sollGroessen, m.groessen)) {
|
||
console.log(` ACHTUNG: Bei „${anordnung}" stimmt das Grauwerden nicht.`);
|
||
ausgang = 1;
|
||
} else if (!m.hinweis.includes(wort)) {
|
||
console.log(` ACHTUNG: Der Hinweis sagt nichts von „${wort}".`);
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
await roh("/workspace/api/reaktion/layout", {
|
||
method: "POST", keks: k.admin, rumpf: { layout: "kino", kamera_ecke: "ur" },
|
||
});
|
||
await host.seite.waitForTimeout(700);
|
||
}
|
||
|
||
/* ---- Die Regieleiste ueber dem Saal -----------------------------
|
||
|
||
Filipe, 28.09.2026: „ich haette das gerne oben also nicht in dieser
|
||
kachel sondern ueber der kachel wo die videos laufen werden."
|
||
|
||
GEMESSEN WIRD, WAS SIE KOSTET UND WAS SIE KANN. Eine Leiste am
|
||
oberen Rand nimmt Hoehe -- und zwar dem Video, um das es geht.
|
||
Zwei Zeilen statt einer waeren rund 90 Pixel, und das sieht man
|
||
auf dem Bildschirmfoto nicht, weil beides „eine Leiste" ist. */
|
||
console.log("");
|
||
console.log(" --- Die Regieleiste ---");
|
||
{
|
||
const leiste = await host.seite.evaluate(() => {
|
||
const l = document.getElementById("regieleiste");
|
||
const r = document.getElementById("reiter");
|
||
const kino = document.getElementById("teil-live");
|
||
if (!l || !r || !kino) return null;
|
||
const lr = l.getBoundingClientRect();
|
||
const kr = kino.getBoundingClientRect();
|
||
const knoepfe = [...r.querySelectorAll(".reiter__knopf")];
|
||
return {
|
||
hoch: Math.round(lr.height),
|
||
oben: Math.round(lr.top),
|
||
/* STEHT SIE WIRKLICH UEBER DEM KINO? Das ist die Frage, nicht
|
||
ob sie irgendwo existiert. */
|
||
ueberDemKino: lr.bottom <= kr.top + 1,
|
||
knoepfe: knoepfe.length,
|
||
/* EINE ZEILE ODER ZWEI? Gemessen an den Oberkanten -- wenn
|
||
zwei Knoepfe verschieden hoch sitzen, bricht sie um. */
|
||
zeilen: new Set(knoepfe.map((k) => Math.round(k.getBoundingClientRect().top))).size,
|
||
kleinster: Math.min(...knoepfe.map((k) => Math.round(k.getBoundingClientRect().height))),
|
||
rollt: r.scrollWidth > r.clientWidth,
|
||
/* Genau EIN Register in der Tabulatorfolge. */
|
||
imTabulator: knoepfe.filter((k) => k.tabIndex === 0).length,
|
||
marke: (() => {
|
||
const m = document.getElementById("reiter-marke");
|
||
if (!m || m.hidden) return null;
|
||
const mr = m.getBoundingClientRect();
|
||
const a = knoepfe.find((k) => k.getAttribute("aria-selected") === "true");
|
||
const ar = a.getBoundingClientRect();
|
||
return {
|
||
breit: Math.round(mr.width),
|
||
/* Sitzt sie unter dem gewaehlten Register? */
|
||
versatz: Math.round(Math.abs(mr.left - ar.left)),
|
||
gleichBreit: Math.abs(mr.width - ar.width) < 2,
|
||
};
|
||
})(),
|
||
};
|
||
});
|
||
|
||
if (!leiste) {
|
||
console.log(" ACHTUNG: Die Regieleiste gibt es nicht.");
|
||
ausgang = 1;
|
||
} else {
|
||
console.log(` ${leiste.knoepfe} Register in ${leiste.zeilen} Zeile(n), `
|
||
+ `Leiste ${leiste.hoch} px hoch bei y=${leiste.oben}, `
|
||
+ `kleinstes Ziel ${leiste.kleinster} px, rollt=${leiste.rollt}`);
|
||
console.log(` Ueber dem Kino: ${leiste.ueberDemKino} · `
|
||
+ `im Tabulator: ${leiste.imTabulator} · `
|
||
+ `Marke ${leiste.marke ? `${leiste.marke.breit} px, Versatz ${leiste.marke.versatz} px` : "(keine)"}`);
|
||
|
||
if (!leiste.ueberDemKino) {
|
||
console.log(" ACHTUNG: Die Leiste steht nicht ueber dem Kino.");
|
||
ausgang = 1;
|
||
}
|
||
if (leiste.zeilen !== 1) {
|
||
console.log(" ACHTUNG: Die Register brechen um -- das kostet das Video Hoehe.");
|
||
ausgang = 1;
|
||
}
|
||
if (leiste.kleinster < 44) {
|
||
console.log(" ACHTUNG: Ein Register ist kleiner als 44 px.");
|
||
ausgang = 1;
|
||
}
|
||
if (leiste.imTabulator !== 1) {
|
||
console.log(" ACHTUNG: Der Tabulator wandert nicht -- sieben Schritte bis zum Inhalt.");
|
||
ausgang = 1;
|
||
}
|
||
if (!leiste.marke || leiste.marke.versatz > 2 || !leiste.marke.gleichBreit) {
|
||
console.log(" ACHTUNG: Die Marke sitzt nicht unter dem gewaehlten Register.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* ---- DIE TASTATUR MUSS TUN, WAS `role=tablist` VERSPRICHT ----
|
||
Ein Versprechen im Markup, das das Programm nicht einloest, ist
|
||
schlimmer als gar keines: Der Vorleser sagt etwas an, was nicht
|
||
stimmt. Also wird es gedrueckt, nicht gelesen. */
|
||
await host.seite.evaluate(() => {
|
||
document.querySelector('#reiter .reiter__knopf[data-tafel="sendung"]').focus();
|
||
});
|
||
const tasten = [];
|
||
for (const [taste, wohin] of [["ArrowRight", "rechts"], ["ArrowRight", "rechts"],
|
||
["ArrowLeft", "links"], ["End", "ans Ende"], ["Home", "an den Anfang"]]) {
|
||
await host.seite.keyboard.press(taste);
|
||
await host.seite.waitForTimeout(220);
|
||
const wo = await host.seite.evaluate(() => {
|
||
const a = document.activeElement;
|
||
const g = [...document.querySelectorAll("#reiter .reiter__knopf")]
|
||
.find((k) => k.getAttribute("aria-selected") === "true");
|
||
const t = [...document.querySelectorAll(".pult-tafel")].find((x) => !x.hidden);
|
||
return {
|
||
fokus: a?.dataset?.tafel || "(?)",
|
||
gewaehlt: g?.dataset?.tafel || "(?)",
|
||
tafel: t?.dataset?.tafel || "(keine)",
|
||
};
|
||
});
|
||
tasten.push(`${wohin}->${wo.fokus}`);
|
||
/* FOKUS, AUSWAHL UND OFFENE TAFEL MUESSEN DASSELBE SAGEN.
|
||
Laufen sie auseinander, zeigt die Regie eine andere Tafel als
|
||
das hervorgehobene Register -- und man stellt etwas ein, von
|
||
dem man glaubt, es gehoere woandershin. */
|
||
if (wo.fokus !== wo.gewaehlt || wo.gewaehlt !== wo.tafel) {
|
||
console.log(` ACHTUNG: Fokus ${wo.fokus}, gewaehlt ${wo.gewaehlt}, `
|
||
+ `offen ${wo.tafel} -- das passt nicht zusammen.`);
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
console.log(` Tastatur: ${tasten.join(" · ")}`);
|
||
|
||
/* ---- EIN REGISTER MACHT DIE ZUGEKLAPPTE REGIE AUF ----
|
||
Seit die Leiste oben steht, kann das Pult zu sein, waehrend man
|
||
ein Register waehlt. Ohne das waere der Knopf einer, bei dem
|
||
sichtbar nichts passiert. */
|
||
await host.seite.click("#pult-griff").catch(() => { });
|
||
await host.seite.waitForTimeout(400);
|
||
const zu = await host.seite.evaluate(() =>
|
||
document.getElementById("pult").dataset.auf);
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="ton"]').catch(() => { });
|
||
await host.seite.waitForTimeout(500);
|
||
const danach = await host.seite.evaluate(() => ({
|
||
auf: document.getElementById("pult").dataset.auf,
|
||
tafel: [...document.querySelectorAll(".pult-tafel")].find((x) => !x.hidden)?.dataset.tafel,
|
||
sichtbar: (() => {
|
||
const t = document.getElementById("tafel-ton");
|
||
const r = t.getBoundingClientRect();
|
||
return r.height > 0 && r.top < window.innerHeight;
|
||
})(),
|
||
}));
|
||
console.log(` Regie zugeklappt (auf=${zu}), Register „Ton" gedrueckt `
|
||
+ `-> auf=${danach.auf}, offen=${danach.tafel}, sichtbar=${danach.sichtbar}`);
|
||
if (zu !== "nein" || danach.auf !== "ja" || !danach.sichtbar) {
|
||
console.log(" ACHTUNG: Ein Register macht die zugeklappte Regie nicht auf.");
|
||
ausgang = 1;
|
||
}
|
||
await bild(host.seite, "20-regieleiste");
|
||
|
||
/* ---- WAS SIE DEM VIDEO KOSTET ----
|
||
Die eigentliche Frage bei einer Leiste am oberen Rand. */
|
||
const platz = await host.seite.evaluate(() => {
|
||
const l = document.getElementById("regieleiste").getBoundingClientRect();
|
||
const k = document.getElementById("teil-live").getBoundingClientRect();
|
||
const s2 = document.getElementById("saal").getBoundingClientRect();
|
||
return {
|
||
leiste: Math.round(l.height), kino: Math.round(k.height), saal: Math.round(s2.height),
|
||
ueber: Math.max(0, document.documentElement.scrollHeight
|
||
- document.documentElement.clientHeight),
|
||
};
|
||
});
|
||
console.log(` Saal ${platz.saal} px = Leiste ${platz.leiste} + Kino ${platz.kino} `
|
||
+ `+ Regie, senkrecht ueber ${platz.ueber} px`);
|
||
if (platz.ueber > 6 || platz.kino < 200) {
|
||
console.log(" ACHTUNG: Die Leiste nimmt dem Bild zu viel oder die Seite laeuft ueber.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* ---- Die Gestaltung der Spendenkarten ---------------------------
|
||
|
||
Filipe: „mach paar fertige und so dass ich auch hochladen kann.
|
||
auch so dass ich das anders gestalten kann oder die größe
|
||
verändern kann."
|
||
|
||
GEMESSEN WIRD DIE KARTE, NICHT DIE EINSTELLUNG. Dass in der
|
||
Datenbank 2.0 steht, sagt nichts darüber, ob die Karte doppelt so
|
||
groß erscheint -- dazwischen liegen eine Zahl im Stilblatt, eine
|
||
Umrechnung in `em` und ein Dutzend Regeln, die noch in `rem`
|
||
stehen könnten. Also: Karte laufen lassen und nachmessen. */
|
||
console.log("");
|
||
console.log(" --- Die Gestaltung der Spendenkarten ---");
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="gestaltung"]').catch(() => {});
|
||
await host.seite.waitForTimeout(700);
|
||
|
||
const leiter = await host.seite.evaluate(() => {
|
||
const zeilen = [...document.querySelectorAll("#stufen .stufe-zeile")];
|
||
return {
|
||
zeilen: zeilen.length,
|
||
ersteName: zeilen[0]?.querySelector(".stufe-name")?.value || "",
|
||
felder: zeilen[0] ? [...zeilen[0].querySelectorAll("input, select")].length : 0,
|
||
/* Jedes Bedienelement muss mit dem Finger zu treffen sein --
|
||
hier wird eingestellt, was vor Publikum im Bild steht. */
|
||
/* VERSTECKTES ZAEHLT NICHT. Hinter jedem „Bild hochladen" liegt
|
||
ein `<input type="file" hidden>` -- es ist 0 px hoch, und das
|
||
ist richtig so: Angefasst wird das Schild davor. Die erste
|
||
Fassung hat es mitgezaehlt und drei Fehler gemeldet, die
|
||
keine waren. Eine Messung, die Unsichtbares misst, misst das
|
||
Falsche. */
|
||
zuKlein: zeilen.flatMap((z) => [...z.querySelectorAll("input, select, .stufe-tun")])
|
||
.filter((e) => !e.hidden && e.offsetParent !== null)
|
||
.filter((e) => e.getBoundingClientRect().height < 40).length,
|
||
ueber: Math.max(0, document.documentElement.scrollWidth
|
||
- document.documentElement.clientWidth),
|
||
};
|
||
});
|
||
console.log(` ${leiter.zeilen} Stufen, erste „${leiter.ersteName}", `
|
||
+ `${leiter.felder} Felder je Zeile, ${leiter.zuKlein} zu kleine Ziele, `
|
||
+ `seitlich ${leiter.ueber} px`);
|
||
if (leiter.zeilen < 3 || leiter.felder < 6 || leiter.zuKlein > 0 || leiter.ueber > 0) {
|
||
console.log(" ACHTUNG: Die Stufenleiter ist unvollstaendig oder laeuft ueber.");
|
||
ausgang = 1;
|
||
}
|
||
await bild(host.seite, "17-stufenleiter");
|
||
|
||
/* ---- Eine Stufe umgestalten und nachsehen, ob es ankommt ---- */
|
||
const vorherKarte = await host.seite.evaluate(async () => {
|
||
/* Erst einmal bei Größe 1 messen -- ohne Bezugswert sagt die zweite
|
||
Messung nichts. */
|
||
const k2 = [...document.querySelectorAll("#stufen .stufe-zeile")][2];
|
||
k2.querySelector(".stufe-groesse").value = "1";
|
||
k2.querySelector(".stufe-groesse").dispatchEvent(new Event("input", { bubbles: true }));
|
||
document.getElementById("stufen-sichern").click();
|
||
return true;
|
||
});
|
||
await host.seite.waitForTimeout(1200);
|
||
|
||
/* ==== ERST AUFRAEUMEN, DANN MESSEN ================================
|
||
|
||
Die erste Fassung hat einfach `document.querySelector(".spenden-
|
||
karte")` genommen -- und damit die Karte der echten 10-Euro-Spende
|
||
erwischt, die aus dem vorigen Messabschnitt noch im Bild stand.
|
||
Ergebnis: drei gemeldete Fehler, die keine waren, darunter „die
|
||
Probe laeuft im ganzen Saal". Eine Messung, die nicht weiss, WELCHE
|
||
Karte sie vor sich hat, misst irgendetwas.
|
||
|
||
Also: beide Tafeln leeren, dann druecken, dann auf eine Karte
|
||
warten, die sich als Probe ausweist. */
|
||
const tafelnLeeren = async () => {
|
||
for (const s2 of [host.seite, zuschauer.seite]) {
|
||
await s2.evaluate(() => {
|
||
for (const k2 of document.querySelectorAll(".spendenkarte")) k2.remove();
|
||
});
|
||
}
|
||
};
|
||
|
||
const messeKarte = async (erwartet) => {
|
||
await tafelnLeeren();
|
||
await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll("#stufen .stufe-zeile")][2];
|
||
[...k2.querySelectorAll(".stufe-tun")].find((x) => x.textContent === "Ausprobieren")?.click();
|
||
});
|
||
/* ==== AUF DIE RICHTIGE KARTE WARTEN, NICHT AUF DIE UHR ==========
|
||
|
||
Zwei Fallen auf einmal, und die zweite hat die erste Fassung
|
||
gekostet:
|
||
|
||
(1) Die Karten laufen in einer SCHLANGE. Ein Knoten aus dem
|
||
Dokument zu nehmen beendet nicht, was die Seite gerade
|
||
zeigt -- die naechste Karte kommt erst, wenn die Dauer der
|
||
vorigen abgelaufen ist. Bei elf Sekunden Dauer und acht
|
||
Sekunden Wartezeit kam sie nie, und die Messung meldete
|
||
einen Fehler, den es nicht gab.
|
||
|
||
(2) Gewartet wird auf die Karte MIT DER ERWARTETEN GROESSE.
|
||
Sonst koennte die vorige Probe die Antwort geben, und die
|
||
Messung bestaetigte eine Aenderung, die gar nicht
|
||
angekommen ist. */
|
||
await host.seite.waitForFunction(
|
||
(soll) => [...document.querySelectorAll('.spendenkarte[data-da="ja"]')].some((k2) =>
|
||
k2.querySelector(".spendenkarte__wer")?.textContent === "Probe"
|
||
&& Math.abs(parseFloat(getComputedStyle(k2).fontSize) - soll) < 0.5),
|
||
erwartet, { timeout: 16000 },
|
||
).catch(() => {});
|
||
/* DIE KARTE KOMMT VON LINKS HEREIN (450 ms). Wer sofort misst,
|
||
misst einen Zwischenschritt: Waehrend des Hereinkommens steht
|
||
sie 28 px weiter links und ist halb durchsichtig. Ein
|
||
Bildschirmfoto aus dieser Phase sieht aus, als waere sie
|
||
abgeschnitten -- und genau das hat mich beim ersten Ansehen in
|
||
die Irre gefuehrt. */
|
||
await host.seite.waitForTimeout(700);
|
||
return host.seite.evaluate(() => {
|
||
/* `data-da="ja"` HEISST: GANZ DA. Beim Hereinkommen UND beim
|
||
Verschwinden steht die Karte auf `translateX(-28px) scale(.96)`
|
||
-- gemessen wurden dadurch 413 statt 430 Pixel (genau das
|
||
0,96-fache) und ein linker Rand von -1. Beides war kein
|
||
Fehler der Karte, sondern ein Blick zur falschen Zeit. */
|
||
const k2 = [...document.querySelectorAll('.spendenkarte[data-da="ja"]')]
|
||
.find((x) => x.querySelector(".spendenkarte__wer")?.textContent === "Probe");
|
||
if (!k2) return null;
|
||
const r = k2.getBoundingClientRect();
|
||
const c = getComputedStyle(k2);
|
||
const betrag = k2.querySelector(".spendenkarte__betrag");
|
||
return {
|
||
schrift: Math.round(parseFloat(c.fontSize) * 100) / 100,
|
||
breit: Math.round(r.width), hoch: Math.round(r.height),
|
||
betragText: betrag?.textContent || "",
|
||
betragSchrift: Math.round(parseFloat(getComputedStyle(betrag).fontSize) * 100) / 100,
|
||
stufe: k2.querySelector(".spendenkarte__stufe")?.textContent || "",
|
||
wer: k2.querySelector(".spendenkarte__wer")?.textContent || "",
|
||
links: Math.round(r.left), oben: Math.round(r.top),
|
||
/* ALLE VIER SEITEN. Die erste Fassung hat nur rechts und unten
|
||
geprueft -- eine Karte, die links heraussteht, waere
|
||
durchgegangen. Im Stream ist links genauso abgeschnitten wie
|
||
rechts. */
|
||
imBild: r.left >= -1 && r.top >= -1
|
||
&& r.right <= window.innerWidth + 1 && r.bottom <= window.innerHeight + 1,
|
||
};
|
||
});
|
||
};
|
||
|
||
const kKlein = await messeKarte(16);
|
||
console.log(` Größe 1: Karte ${kKlein?.breit}x${kKlein?.hoch} px, Schrift ${kKlein?.schrift} px, `
|
||
+ `Betrag „${kKlein?.betragText}" bei ${kKlein?.betragSchrift} px, Stufe „${kKlein?.stufe}"`);
|
||
if (!kKlein || kKlein.wer !== "Probe") {
|
||
console.log(" ACHTUNG: Die Probe zeigt keine Karte -- oder sie ist nicht als Probe erkennbar.");
|
||
ausgang = 1;
|
||
}
|
||
await bild(host.seite, "18-karte-normal");
|
||
|
||
/* DIE PROBE DARF NUR BEIM DRUECKENDEN ERSCHEINEN. Eine Probe, die im
|
||
ganzen Saal laeuft, ist eine Spende, die es nicht gab. */
|
||
const beimZuschauer = await zuschauer.seite.evaluate(() =>
|
||
document.querySelectorAll(".spendenkarte").length);
|
||
console.log(` Karten bei der Zuschauerin waehrend der Probe: ${beimZuschauer} (soll: 0)`);
|
||
if (beimZuschauer !== 0) {
|
||
console.log(" ACHTUNG: Die Probe laeuft im ganzen Saal.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* Jetzt auf doppelte Größe -- und nachmessen. */
|
||
await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll("#stufen .stufe-zeile")][2];
|
||
k2.querySelector(".stufe-groesse").value = "2";
|
||
k2.querySelector(".stufe-groesse").dispatchEvent(new Event("input", { bubbles: true }));
|
||
k2.querySelector(".stufe-farbe").value = "#55d6a0";
|
||
k2.querySelector(".stufe-farbe").dispatchEvent(new Event("input", { bubbles: true }));
|
||
document.getElementById("stufen-sichern").click();
|
||
});
|
||
await host.seite.waitForTimeout(1400);
|
||
const kGross = await messeKarte(32);
|
||
console.log(` Größe 2: Karte ${kGross?.breit}x${kGross?.hoch} px, Schrift ${kGross?.schrift} px, `
|
||
+ `Betrag bei ${kGross?.betragSchrift} px, links ${kGross?.links} / oben ${kGross?.oben}, `
|
||
+ `ganz im Bild=${kGross?.imBild}`);
|
||
await bild(host.seite, "19-karte-doppelt");
|
||
|
||
if (!kGross || !kKlein) {
|
||
console.log(" ACHTUNG: Die Karte liess sich nicht zweimal messen.");
|
||
ausgang = 1;
|
||
} else {
|
||
/* ZWEIFACH HEISST ZWEIFACH. Wenn eine einzige Regel noch in `rem`
|
||
stünde statt in `em`, wüchse die Karte ungleichmäßig -- und das
|
||
sieht man auf einem Bildschirmfoto nicht, weil beides „größer"
|
||
aussieht. */
|
||
const faktor = kGross.betragSchrift / kKlein.betragSchrift;
|
||
console.log(` Der Betrag wuchs um das ${faktor.toFixed(2)}-fache (soll: 2,00)`);
|
||
if (Math.abs(faktor - 2) > 0.05) {
|
||
console.log(" ACHTUNG: Die Größe wirkt nicht oder nicht gleichmaessig.");
|
||
ausgang = 1;
|
||
}
|
||
if (kGross.hoch <= kKlein.hoch) {
|
||
console.log(" ACHTUNG: Die Karte wird nicht hoeher.");
|
||
ausgang = 1;
|
||
}
|
||
/* UND SIE MUSS INS BILD PASSEN. Die Obergrenze 2,5 ist daran
|
||
ausgerichtet -- eine Karte, die rechts heraussteht, ist im
|
||
Stream abgeschnitten. */
|
||
if (!kGross.imBild) {
|
||
console.log(" ACHTUNG: Die doppelt grosse Karte steht aus dem Bild heraus.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* ---- Und die Farbe? ---- */
|
||
const farbe = await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll('.spendenkarte[data-da="ja"]')]
|
||
.find((x) => x.querySelector(".spendenkarte__wer")?.textContent === "Probe");
|
||
return k2 ? getComputedStyle(k2).getPropertyValue("--sp-farbe").trim() : "";
|
||
});
|
||
console.log(` Farbe der Karte: „${farbe}" (gesetzt war #55d6a0)`);
|
||
if (farbe.toLowerCase() !== "#55d6a0") {
|
||
console.log(" ACHTUNG: Die Farbe kommt nicht an.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* Wieder auf einen vernuenftigen Wert -- die naechste Messung soll
|
||
nicht mit einer Karte in doppelter Groesse anfangen. */
|
||
await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll("#stufen .stufe-zeile")][2];
|
||
k2.querySelector(".stufe-groesse").value = "1.45";
|
||
k2.querySelector(".stufe-groesse").dispatchEvent(new Event("input", { bubbles: true }));
|
||
k2.querySelector(".stufe-farbe").value = "#e0a13a";
|
||
k2.querySelector(".stufe-farbe").dispatchEvent(new Event("input", { bubbles: true }));
|
||
document.getElementById("stufen-sichern").click();
|
||
});
|
||
await host.seite.waitForTimeout(1000);
|
||
|
||
/* ---- Das Video: Tempo, Anordnung, Umschalter --------------------
|
||
|
||
Filipe: „auch bei den videos sachen wie pause geschwindigkeit ...
|
||
nur mein bild, nur das video ... auch gerne so dass ich vielleicht
|
||
noch ein zweites nebenbei vorbereiten kann."
|
||
|
||
GEMESSEN WIRD DIE WIRKUNG, NICHT DIE EINSTELLUNG. Dass in der
|
||
Datenbank 1.5 steht, sagt nichts darüber, ob das Video schneller
|
||
läuft -- YouTube ignoriert eine unmögliche Geschwindigkeit
|
||
schweigend. Also wird die Uhr zweimal abgelesen und nachgerechnet. */
|
||
console.log("");
|
||
console.log(" --- Video: Tempo, Anordnung, Umschalter ---");
|
||
|
||
/* Die Regie zeigt „1:23 / 4:56". Das ist die einzige Stelle, an der
|
||
der Spielstand als sichtbarer Text steht -- und damit die einzige,
|
||
an der man von aussen nachrechnen kann. */
|
||
const uhrLesen = (seite) => seite.evaluate(() => {
|
||
const t = document.getElementById("v-zeit")?.textContent || "";
|
||
const [links] = t.split("/");
|
||
const [m, sek] = String(links).trim().split(":").map(Number);
|
||
return Number.isFinite(m) && Number.isFinite(sek) ? m * 60 + sek : null;
|
||
});
|
||
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="sendung"]').catch(() => {});
|
||
await host.seite.waitForTimeout(600);
|
||
|
||
const tempoKnoepfe = await host.seite.evaluate(() =>
|
||
[...document.querySelectorAll("#tempo .tempo__knopf")].map((k2) => ({
|
||
wort: k2.textContent.trim(),
|
||
hoch: Math.round(k2.getBoundingClientRect().height),
|
||
breit: Math.round(k2.getBoundingClientRect().width),
|
||
})));
|
||
console.log(` ${tempoKnoepfe.length} Tempo-Knoepfe: `
|
||
+ tempoKnoepfe.map((t) => t.wort).join(" "));
|
||
if (tempoKnoepfe.length !== 6 || tempoKnoepfe.some((t) => t.breit < 44)) {
|
||
console.log(" ACHTUNG: Die Tempo-Knoepfe fehlen oder sind zu schmal.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* NORMALES TEMPO ZUERST MESSEN -- als Bezugswert. Ohne ihn wüsste man
|
||
nicht, ob 1,5x wirklich schneller ist oder ob das Video einfach
|
||
schon lief. */
|
||
await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll("#tempo .tempo__knopf")]
|
||
.find((x) => Number(x.dataset.tempo) === 1);
|
||
if (k2) k2.click();
|
||
});
|
||
await host.seite.waitForTimeout(1500);
|
||
const a1 = await uhrLesen(host.seite);
|
||
await host.seite.waitForTimeout(6000);
|
||
const a2 = await uhrLesen(host.seite);
|
||
|
||
await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll("#tempo .tempo__knopf")]
|
||
.find((x) => Number(x.dataset.tempo) === 2);
|
||
if (k2) k2.click();
|
||
});
|
||
await host.seite.waitForTimeout(1800);
|
||
const b1 = await uhrLesen(host.seite);
|
||
await host.seite.waitForTimeout(6000);
|
||
const b2 = await uhrLesen(host.seite);
|
||
|
||
const normal = (a1 !== null && a2 !== null) ? (a2 - a1) / 6 : null;
|
||
const schnell = (b1 !== null && b2 !== null) ? (b2 - b1) / 6 : null;
|
||
console.log(` 1x: ${normal === null ? "?" : normal.toFixed(2)} Sekunden je Sekunde · `
|
||
+ `2x: ${schnell === null ? "?" : schnell.toFixed(2)}`);
|
||
if (normal === null || schnell === null || schnell < normal * 1.5) {
|
||
console.log(" ACHTUNG: Die Geschwindigkeit wirkt nicht -- das Video laeuft gleich schnell.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* UND DER ZUSCHAUER? Er muss es SEHEN -- sonst haelt er seine Leitung
|
||
fuer kaputt. */
|
||
await zuschauer.seite.waitForTimeout(1200);
|
||
const schild = await zuschauer.seite.evaluate(() => {
|
||
const s2 = document.getElementById("leinwand-tempo");
|
||
return {
|
||
versteckt: !!s2?.hidden, wort: s2?.textContent || "",
|
||
amKino: document.getElementById("teil-live")?.dataset.tempo,
|
||
};
|
||
});
|
||
console.log(` Die Zuschauerin sieht: „${schild.wort}" `
|
||
+ `(versteckt=${schild.versteckt}, am Kino ${schild.amKino})`);
|
||
if (schild.versteckt || !schild.wort.includes("2") || schild.amKino !== "2") {
|
||
console.log(" ACHTUNG: Der Zuschauerin wird die Geschwindigkeit nicht gezeigt.");
|
||
ausgang = 1;
|
||
}
|
||
await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll("#tempo .tempo__knopf")]
|
||
.find((x) => Number(x.dataset.tempo) === 1);
|
||
if (k2) k2.click();
|
||
});
|
||
await zuschauer.seite.waitForTimeout(1200);
|
||
const wieder = await zuschauer.seite.evaluate(() =>
|
||
!!document.getElementById("leinwand-tempo")?.hidden);
|
||
console.log(` Zurueck auf 1x: Schild wieder weg = ${wieder}`);
|
||
if (!wieder) {
|
||
console.log(" ACHTUNG: Das Schild bleibt stehen, obwohl es normal laeuft.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ---- Die fuenf Anordnungen, beim Zuschauer gemessen ---- */
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="bild"]').catch(() => {});
|
||
await host.seite.waitForTimeout(500);
|
||
const anordnungen = {};
|
||
for (const [name, was] of [
|
||
["kino", "Video groß"], ["gleich", "Gleich groß"], ["kamera", "Kameras groß"],
|
||
["nur_video", "Nur Video"], ["nur_kamera", "Nur Kamera"],
|
||
]) {
|
||
await host.seite.click(`#layout-reihe .layout-knopf[data-layout="${name}"]`).catch(() => {});
|
||
await host.seite.waitForTimeout(1100);
|
||
anordnungen[name] = await zuschauer.seite.evaluate(() => {
|
||
const sicht = (w) => {
|
||
const e = document.querySelector(w);
|
||
if (!e) return "(fehlt)";
|
||
const c = getComputedStyle(e);
|
||
const r = e.getBoundingClientRect();
|
||
return c.display === "none" ? "weg" : `${Math.round(r.width)}x${Math.round(r.height)}`;
|
||
};
|
||
/* LAEUFT ETWAS UEBER? Die Leinwand hat `overflow: hidden` --
|
||
ein Kamerafenster, das daneben liegt, ist damit unsichtbar
|
||
und nicht etwa abgeschnitten sichtbar. Genau so verschwand am
|
||
28.09. bei „nur Kamera“ der zweite Gast komplett: Behaelter
|
||
2175 px breit in einer 1072 px breiten Leinwand. Auf dem Bild
|
||
sah es aus wie eine Anordnung fuer einen -- und niemand haette
|
||
gefragt, wo der zweite geblieben ist. */
|
||
const lein = document.querySelector(".leinwand")?.getBoundingClientRect();
|
||
const kk = document.querySelector(".kameras")?.getBoundingClientRect();
|
||
const fenster = [...document.querySelectorAll(".kamera")];
|
||
return {
|
||
layout: document.getElementById("teil-live")?.dataset.layout,
|
||
video: sicht(".leinwand__video"), kameras: sicht(".kameras"),
|
||
kamera: sicht(".kamera"),
|
||
ueber: (lein && kk) ? Math.round(Math.max(0, kk.width - lein.width)) : 0,
|
||
fenster: fenster.length,
|
||
/* Und liegt auch jedes einzelne innerhalb? */
|
||
drin: !lein ? 0 : fenster.filter((f) => {
|
||
const r = f.getBoundingClientRect();
|
||
return r.left >= lein.left - 1 && r.right <= lein.right + 1
|
||
&& r.top >= lein.top - 1 && r.bottom <= lein.bottom + 1;
|
||
}).length,
|
||
};
|
||
});
|
||
const m = anordnungen[name];
|
||
console.log(` ${was.padEnd(12)} (${name}): Video ${m.video}, `
|
||
+ `Kameras ${m.kameras}, eine ${m.kamera}, ${m.drin}/${m.fenster} im Bild`
|
||
+ (m.ueber ? `, UEBER ${m.ueber} px` : ""));
|
||
if (m.ueber > 0 || (m.fenster > 0 && m.drin !== m.fenster && name !== "nur_video")) {
|
||
console.log(` ACHTUNG: Bei „${was}“ liegt ein Kamerafenster ausserhalb der Leinwand.`);
|
||
ausgang = 1;
|
||
}
|
||
await bild(zuschauer.seite, `15-anordnung-${name}`);
|
||
}
|
||
/* DAS WEGLASSEN MUSS WIRKLICH WEGLASSEN. `display: none` und nicht
|
||
nur klein -- ein unsichtbarer YouTube-Rahmen spielt weiter und
|
||
haelt den Ton. */
|
||
if (anordnungen.nur_video.kameras !== "weg") {
|
||
console.log(" ACHTUNG: Bei „nur Video“ stehen die Kameras noch da.");
|
||
ausgang = 1;
|
||
}
|
||
if (anordnungen.nur_kamera.video !== "weg") {
|
||
console.log(" ACHTUNG: Bei „nur Kamera“ steht das Video noch da.");
|
||
ausgang = 1;
|
||
}
|
||
/* UND DIE KAMERAS MUESSEN DABEI GROSS WERDEN. Ein Weglassen, nach dem
|
||
zwei Briefmarken in einer schwarzen Flaeche stehen, ist keine
|
||
Anordnung, sondern ein Fehler. */
|
||
const klein = Number(String(anordnungen.kino.kamera).split("x")[0]) || 0;
|
||
const gross = Number(String(anordnungen.nur_kamera.kamera).split("x")[0]) || 0;
|
||
console.log(` Kamerabreite: kino ${klein} px -> nur_kamera ${gross} px`);
|
||
if (gross <= klein * 1.5) {
|
||
console.log(" ACHTUNG: Bei „nur Kamera“ wird die Kamera nicht groesser.");
|
||
ausgang = 1;
|
||
}
|
||
await host.seite.click('#layout-reihe .layout-knopf[data-layout="kino"]').catch(() => {});
|
||
await host.seite.waitForTimeout(800);
|
||
|
||
/* ---- Der Umschalter zwischen zwei Videos ---- */
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="sendung"]').catch(() => {});
|
||
await host.seite.waitForTimeout(500);
|
||
const vorherVerborgen = await host.seite.evaluate(() =>
|
||
!!document.getElementById("v-tauschen")?.hidden);
|
||
console.log(` Ohne zweites Video ist der Umschalter verborgen: ${vorherVerborgen}`);
|
||
if (!vorherVerborgen) {
|
||
console.log(" ACHTUNG: Ein Umschalter ohne Gegenstueck ist eine Falle.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* KEIN STILLES VERSCHLUCKEN. `.catch(() => {})` hat hier den
|
||
Grund weggeworfen, warum das zweite Video nie ankam -- und die
|
||
naechste Zeile meldete dann „Der Umschalter kommt nicht", als
|
||
waere das Programm kaputt. Ein Messfehler, der wie ein
|
||
Programmfehler aussieht, kostet mehr Zeit als gar keine Messung. */
|
||
try {
|
||
/* ERST HINSCHAUEN, DANN DRUECKEN. Die Regie rollt senkrecht
|
||
(346 Pixel Platz, 1130 Pixel Inhalt); „Bereitlegen" steht am
|
||
unteren Ende, gemessen bei y=1430 in einem 900 Pixel hohen
|
||
Fenster. Ein Mensch scrollt dorthin, bevor er drueckt -- die
|
||
Messung muss dasselbe tun, sonst misst sie etwas, das niemand
|
||
erlebt. */
|
||
await host.seite.locator("#p-zweit").scrollIntoViewIfNeeded();
|
||
await host.seite.fill("#p-zweit", "https://youtu.be/9bZkp7q19f0");
|
||
await host.seite.locator("#p-zweit-sichern").scrollIntoViewIfNeeded();
|
||
await host.seite.click("#p-zweit-sichern");
|
||
} catch (f) {
|
||
console.log(` ACHTUNG: das zweite Video liess sich nicht eintragen -- ${f.message.split(String.fromCharCode(10))[0]}`);
|
||
ausgang = 1;
|
||
}
|
||
await host.seite.waitForTimeout(2600);
|
||
const umschalter = await host.seite.evaluate(() => {
|
||
const k2 = document.getElementById("v-tauschen");
|
||
const r = k2.getBoundingClientRect();
|
||
return {
|
||
versteckt: !!k2.hidden, wort: k2.textContent.replace(/\s+/g, " ").trim(),
|
||
breit: Math.round(r.width), hoch: Math.round(r.height),
|
||
laeuftJetzt: document.getElementById("spur-titel")?.textContent || "",
|
||
};
|
||
});
|
||
console.log(` Umschalter: „${umschalter.wort}" (${umschalter.breit}x${umschalter.hoch} px)`);
|
||
console.log(` Es laeuft: „${umschalter.laeuftJetzt}"`);
|
||
if (umschalter.versteckt || umschalter.hoch < 40) {
|
||
console.log(" ACHTUNG: Der Umschalter kommt nicht oder ist zu klein.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* EINMAL HIN. Die Stelle muss sich merken -- ohne das waere „hin und
|
||
her" ein Neustart von vorn, und dann schaltet niemand zweimal. */
|
||
const stelleVorher = await uhrLesen(host.seite);
|
||
await host.seite.click("#v-tauschen");
|
||
await host.seite.waitForTimeout(3000);
|
||
const nachHin = await host.seite.evaluate(() => ({
|
||
titel: document.getElementById("spur-titel")?.textContent || "",
|
||
zurueckZu: document.getElementById("v-tausch-titel")?.textContent || "",
|
||
zeit: document.getElementById("v-zeit")?.textContent || "",
|
||
}));
|
||
console.log(` Nach dem Wechseln laeuft: „${nachHin.titel}", zurueck zu „${nachHin.zurueckZu}"`);
|
||
|
||
/* UND ZURUECK. */
|
||
await host.seite.click("#v-tauschen");
|
||
await host.seite.waitForTimeout(3000);
|
||
const nachZurueck = await host.seite.evaluate(() => ({
|
||
titel: document.getElementById("spur-titel")?.textContent || "",
|
||
zeit: document.getElementById("v-zeit")?.textContent || "",
|
||
}));
|
||
const stelleNachher = await uhrLesen(host.seite);
|
||
console.log(` Zurueck bei: „${nachZurueck.titel}", Uhr ${nachZurueck.zeit} `
|
||
+ `(vorher war ${stelleVorher} s, jetzt ${stelleNachher} s)`);
|
||
if (nachHin.titel === nachZurueck.titel) {
|
||
console.log(" ACHTUNG: Der Umschalter wechselt nicht.");
|
||
ausgang = 1;
|
||
}
|
||
/* Die gemerkte Stelle: etwa dort weiter, wo es aufgehoert hat --
|
||
sechs Sekunden Spiel fuer das Laden des Videos. */
|
||
if (stelleVorher === null || stelleNachher === null
|
||
|| Math.abs(stelleNachher - stelleVorher) > 8) {
|
||
console.log(` ACHTUNG: Die gemerkte Stelle ging verloren `
|
||
+ `(${stelleVorher} -> ${stelleNachher}).`);
|
||
ausgang = 1;
|
||
}
|
||
await bild(host.seite, "16-umschalter");
|
||
await host.seite.fill("#p-zweit", "").catch(() => {});
|
||
await host.seite.click("#p-zweit-sichern").catch(() => {});
|
||
await host.seite.waitForTimeout(900);
|
||
|
||
console.log("");
|
||
console.log(" --- Rollen und Moderation ---");
|
||
const modi = await alsWer("modi", 1440, 900);
|
||
await modi.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "domcontentloaded" });
|
||
await modi.seite.waitForTimeout(2500);
|
||
|
||
const abz = await modi.seite.evaluate(() => {
|
||
const liste = document.getElementById("chat-liste");
|
||
const marken = [...liste.querySelectorAll(".beitrag__rolle")].map((m) => ({
|
||
wort: m.textContent.trim(),
|
||
rolle: m.dataset.rolle,
|
||
schrift: Math.round(parseFloat(getComputedStyle(m).fontSize) * 100) / 100,
|
||
breit: Math.round(m.getBoundingClientRect().width),
|
||
}));
|
||
/* LAEUFT EINE ZEILE UEBER? Nicht die Zahl aus dem Stilblatt, die
|
||
tatsaechliche Breite gegen die tatsaechliche Liste. */
|
||
const innen = liste.getBoundingClientRect();
|
||
const ueber = [...liste.querySelectorAll(".beitrag")]
|
||
.map((b) => Math.round(b.scrollWidth - b.clientWidth))
|
||
.filter((x) => x > 0);
|
||
/* Und die Kopfzeile selbst -- dort stehen Name, Abzeichen und
|
||
Knopf nebeneinander. */
|
||
const koepfe = [...liste.querySelectorAll(".beitrag__wer")]
|
||
.map((h) => Math.round(h.scrollWidth - h.clientWidth))
|
||
.filter((x) => x > 0);
|
||
return {
|
||
marken, ueber, koepfe,
|
||
listenbreite: Math.round(innen.width),
|
||
namensknoepfe: liste.querySelectorAll(".beitrag__name").length,
|
||
beitraege: liste.children.length,
|
||
};
|
||
});
|
||
console.log(` Schiene ${abz.listenbreite} px breit, ${abz.beitraege} Beitraege, `
|
||
+ `${abz.marken.length} Abzeichen, ${abz.namensknoepfe} Namen zum Anfassen.`);
|
||
for (const m of abz.marken) {
|
||
console.log(` „${m.wort}" (${m.rolle}) ${m.schrift} px, ${m.breit} px breit`);
|
||
}
|
||
if (abz.marken.length < 3) {
|
||
console.log(" ACHTUNG: Die Abzeichen kommen im Chat nicht an.");
|
||
ausgang = 1;
|
||
}
|
||
/* 11,5 px ist die Untergrenze des Hauses (pruef-css-klassen). */
|
||
if (abz.marken.some((m) => m.schrift < 11.5)) {
|
||
console.log(" ACHTUNG: Ein Abzeichen ist kleiner als 11,5 px.");
|
||
ausgang = 1;
|
||
}
|
||
if (abz.ueber.length || abz.koepfe.length) {
|
||
console.log(` ACHTUNG: ${abz.ueber.length} Beitraege und ${abz.koepfe.length} Kopfzeilen `
|
||
+ `laufen ueber (${[...abz.ueber, ...abz.koepfe].join(", ")} px).`);
|
||
ausgang = 1;
|
||
}
|
||
/* EIN MODI MELDET NICHT -- er handelt. Der Melden-Knopf gehoert
|
||
denen, die nichts anderes tun koennen. */
|
||
const modiSicht = await modi.seite.evaluate(() =>
|
||
document.querySelectorAll("#chat-liste .beitrag__melden").length);
|
||
console.log(` Der Modi sieht ${modiSicht} Melden-Knoepfe (soll: 0) `
|
||
+ `und ${abz.namensknoepfe} Namen zum Anfassen.`);
|
||
if (modiSicht !== 0 || abz.namensknoepfe < 3) {
|
||
console.log(" ACHTUNG: Melden und Moderieren sind vertauscht.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* DAS MENUE MUSS INS BILD PASSEN. Ein Menue, das unten oder rechts
|
||
heraussteht, ist auf dem Handy die Haelfte der Zeit nicht
|
||
bedienbar -- und man sieht es erst, wenn man es aufmacht. */
|
||
/* ==== AUF DAS MENUE WARTEN, NICHT AUF DIE UHR ===================
|
||
|
||
Hier stand `click(...).catch(() => {})` und danach 400 ms Pause.
|
||
Beide Haelften waren falsch:
|
||
|
||
Der `.catch()` verschluckte einen fehlgeschlagenen Klick still.
|
||
Die Chatliste wird bei jeder Meldung neu gezeichnet, und trifft
|
||
das genau zwischen Auswahl und Klick, ist der Knoten weg.
|
||
|
||
Und 400 ms sind eine Wette. Gemessen: Derselbe Lauf war am
|
||
28.09. dreimal gruen und beim vierten Mal rot -- geaendert
|
||
hatte sich am Programm nichts, nur an dieser Messung. Ein
|
||
Ergebnis, das ohne Codeaenderung kippt, ist ein Fehler der
|
||
Messung und kein Befund.
|
||
|
||
Jetzt wird auf das MERKMAL gewartet und bis zu dreimal neu
|
||
angesetzt -- und die Zahl der Versuche steht im Protokoll. Ein
|
||
stiller Wiederholungsversuch waere nur eine leisere Wette. */
|
||
let menueVersuche = 0;
|
||
let menueOffen = false;
|
||
let letzterGrund = "";
|
||
/* DER KLICK IST EIN UMSCHALTER -- ALSO ERST NACHSEHEN, DANN KLICKEN.
|
||
Die Schleife klickte bis zu dreimal blind. Der erste Klick
|
||
oeffnet das Menue, der zweite SCHLIESST es wieder, der dritte
|
||
oeffnet es erneut. Gemessen am 28.09.2026: Eine eingebaute Spur
|
||
zeigte „nach dem Klick Menue: true", waehrend die Messung
|
||
danebenstehend „geht nicht auf" meldete -- sie hatte es selbst
|
||
zugeklickt. Ein Wiederholungsversuch, der den Zustand aendert,
|
||
den er pruefen soll, misst am Ende sich selbst. */
|
||
while (menueVersuche < 3 && !menueOffen) {
|
||
menueVersuche += 1;
|
||
const schonOffen = await modi.seite.evaluate(() => !!document.querySelector(".modmenue"));
|
||
if (!schonOffen) {
|
||
/* IM BROWSER KLICKEN, NICHT VON AUSSEN.
|
||
Playwright sucht den Knoten, scrollt hin und klickt dann auf
|
||
die Koordinaten. Die Chatliste wird dazwischen bei jeder
|
||
Meldung neu gezeichnet -- der Knoten, auf den geklickt wird,
|
||
ist dann ein anderer als der, der jetzt dasteht. Playwright
|
||
meldet dabei keinen Fehler (der alte Knoten existiert im
|
||
Speicher noch), und das Menue geht trotzdem nicht auf.
|
||
Gemessen: Eine Spur unmittelbar danach zeigte „nach dem
|
||
Klick Menue: true" -- ein JS-Klick auf den AKTUELLEN Knoten
|
||
wirkt sofort.
|
||
Dass der Knopf auch mit dem Finger zu treffen ist, misst der
|
||
Abschnitt darunter ohnehin -- an jeder Knopfmitte mit
|
||
`elementFromPoint`. */
|
||
const ging = await modi.seite.evaluate(() => {
|
||
const ziel = [...document.querySelectorAll("#chat-liste .beitrag__name")]
|
||
.filter((e) => e.textContent.trim() === "Nachtfalter").pop();
|
||
if (!ziel) return "kein Beitrag von Nachtfalter in der Liste";
|
||
ziel.scrollIntoView({ block: "center" });
|
||
ziel.click();
|
||
return "";
|
||
});
|
||
if (ging) letzterGrund = ging;
|
||
}
|
||
/* `attached`, NICHT `visible`. Ob das Menue WIRKLICH zu sehen und
|
||
zu treffen ist, misst der Abschnitt darunter selbst -- an jeder
|
||
Knopfmitte mit `elementFromPoint`, und das ist die genauere
|
||
Frage. Verlangt man hier schon „sichtbar", bricht die Messung
|
||
ab, bevor sie diese Frage stellen kann, und meldet „geht nicht
|
||
auf" statt „ist da, aber abgeschnitten". */
|
||
menueOffen = await modi.seite.waitForSelector(".modmenue", { state: "attached", timeout: 3000 })
|
||
.then(() => true).catch(() => false);
|
||
}
|
||
if (menueVersuche > 1) {
|
||
console.log(` (Das Menue brauchte ${menueVersuche} Anlaeufe -- die Chatliste `
|
||
+ `wurde dazwischen neu gezeichnet.)`);
|
||
}
|
||
/* OEFFNEN UND MESSEN IN EINEM ZUG.
|
||
Hier stand eine Pause von 250 ms zwischen Klick und Messung. Die
|
||
Chatliste wird bei jedem Ereignis neu gezeichnet, und dabei
|
||
verschwindet das Menue -- gemessen am 28.09.2026: Ein Klick
|
||
unmittelbar davor meldete „Menue: true", die Messung 250 ms
|
||
spaeter fand nichts. Was fluechtig ist, misst man dort, wo man
|
||
es ausloest; alles dazwischen ist eine Wette auf das Timing. */
|
||
const menue = await modi.seite.evaluate(() => {
|
||
let m = document.querySelector(".modmenue");
|
||
if (!m) {
|
||
const ziel = [...document.querySelectorAll("#chat-liste .beitrag__name")]
|
||
.filter((e) => e.textContent.trim() === "Nachtfalter").pop();
|
||
if (ziel) { ziel.scrollIntoView({ block: "center" }); ziel.click(); }
|
||
m = document.querySelector(".modmenue");
|
||
}
|
||
if (!m) return null;
|
||
const r = m.getBoundingClientRect();
|
||
const knoepfe = [...m.querySelectorAll(".modmenue__knopf")].map((k2) => ({
|
||
wort: k2.textContent.trim(),
|
||
hoch: Math.round(k2.getBoundingClientRect().height),
|
||
}));
|
||
/* IST ES WIRKLICH ZU SEHEN? „Im Fenster" reicht nicht als Frage.
|
||
Beim ersten Bau lag das Menue nachweislich komplett im Fenster
|
||
(x=1099..1315 von 1440, y=652..840 von 900) und war trotzdem
|
||
abgeschnitten -- die Chatliste rollt, und ein rollender
|
||
Vorfahre beschneidet sein Kind. Gefunden hat das nicht die
|
||
Messung, sondern das Bildschirmfoto.
|
||
Deshalb jetzt der einzige Test, der die Frage beantwortet: An
|
||
die Stelle jedes Knopfes fassen und nachsehen, was dort
|
||
tatsaechlich liegt. */
|
||
const greifbar = [...m.querySelectorAll(".modmenue__knopf")].filter((k2) => {
|
||
const q = k2.getBoundingClientRect();
|
||
const oben = document.elementFromPoint(q.left + q.width / 2, q.top + q.height / 2);
|
||
return !!oben && (k2 === oben || k2.contains(oben));
|
||
}).length;
|
||
return {
|
||
links: Math.round(r.left), rechts: Math.round(r.right),
|
||
oben: Math.round(r.top), unten: Math.round(r.bottom),
|
||
breit: Math.round(r.width), knoepfe, greifbar,
|
||
fensterBreit: window.innerWidth, fensterHoch: window.innerHeight,
|
||
};
|
||
});
|
||
if (!menue) {
|
||
/* MIT GRUND. Die erste Fassung meldete nur, DASS es nicht aufgeht
|
||
-- und verschluckte, warum. Ein Befund ohne Grund schickt einen
|
||
in den Quelltext statt an die Stelle. */
|
||
const beitraege = await modi.seite.evaluate(() => {
|
||
const namen = [...document.querySelectorAll("#chat-liste .beitrag__name")];
|
||
return { n: document.querySelectorAll("#chat-liste .beitrag").length,
|
||
namen: namen.length,
|
||
/* WER STEHT DA? Ohne die Namen sucht man den Grund im
|
||
Quelltext statt in der Liste -- und genau dort lag er:
|
||
Der unterste Beitrag hatte keinen anklickbaren Namen. */
|
||
liste: namen.map((e) => e.textContent.trim()).join(", ") };
|
||
});
|
||
console.log(` Namen in der Liste: ${beitraege.liste}`);
|
||
console.log(` ACHTUNG: Das Moderationsmenue geht nicht auf `
|
||
+ `(${beitraege.n} Beitraege, ${beitraege.namen} anklickbare Namen`
|
||
+ `${letzterGrund ? ", " + letzterGrund : ""}).`);
|
||
ausgang = 1;
|
||
} else {
|
||
console.log(` Menue ${menue.breit} px breit bei x=${menue.links}..${menue.rechts} `
|
||
+ `(Fenster ${menue.fensterBreit}), y=${menue.oben}..${menue.unten}`);
|
||
for (const b of menue.knoepfe) console.log(` „${b.wort}" ${b.hoch} px hoch`);
|
||
console.log(` ${menue.greifbar} von ${menue.knoepfe.length} Knoepfen sind anfassbar`);
|
||
const raus = menue.rechts > menue.fensterBreit || menue.links < 0
|
||
|| menue.unten > menue.fensterHoch;
|
||
if (raus) {
|
||
console.log(" ACHTUNG: Das Menue steht aus dem Bild heraus.");
|
||
ausgang = 1;
|
||
}
|
||
if (menue.greifbar !== menue.knoepfe.length) {
|
||
console.log(" ACHTUNG: Ein Knopf des Menues ist verdeckt oder abgeschnitten.");
|
||
ausgang = 1;
|
||
}
|
||
if (menue.knoepfe.length < 4 || menue.knoepfe.some((b) => b.hoch < 44)) {
|
||
console.log(" ACHTUNG: Ein Knopf im Menue ist kleiner als 44 px.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
await bild(modi.seite, "12-moderationsmenue");
|
||
|
||
/* STUMM SCHALTEN -- und beim Betroffenen nachsehen.
|
||
Der Chat steht dabei auf „offen": genau der Fall, in dem die
|
||
Auskunft vorher falsch war („Der Chat ist gerade zu"). */
|
||
/* AUFMACHEN UND DRUECKEN IN EINEM ZUG -- aus demselben Grund wie
|
||
oben: Zwischen dem Aufmachen und diesem Klick wird die Chatliste
|
||
neu gezeichnet, und dabei verschwindet das Menue. Ein Klick von
|
||
aussen trifft dann nichts, meldet aber auch nichts. */
|
||
const stummGing = await modi.seite.evaluate(() => {
|
||
if (!document.querySelector(".modmenue")) {
|
||
const ziel = [...document.querySelectorAll("#chat-liste .beitrag__name")]
|
||
.filter((e) => e.textContent.trim() === "Nachtfalter").pop();
|
||
if (ziel) { ziel.scrollIntoView({ block: "center" }); ziel.click(); }
|
||
}
|
||
const k2 = [...document.querySelectorAll(".modmenue__knopf")]
|
||
.find((x) => x.textContent.includes("stumm"));
|
||
if (!k2) return "kein Knopf „stumm“ im Menue";
|
||
k2.click();
|
||
return "";
|
||
});
|
||
if (stummGing) {
|
||
console.log(` ACHTUNG: ${stummGing}`);
|
||
ausgang = 1;
|
||
}
|
||
await modi.seite.waitForTimeout(1500);
|
||
|
||
const beimGast = await zuschauer.seite.evaluate(() => {
|
||
const f = document.getElementById("chat-feld");
|
||
return { gesperrt: !!f?.disabled, grund: f?.placeholder || "" };
|
||
});
|
||
console.log(` Die Stummgeschaltete sieht: gesperrt=${beimGast.gesperrt}, `
|
||
+ `Grund „${beimGast.grund}"`);
|
||
const serverSagt = await roh("/workspace/api/reaktion/chat", {
|
||
method: "POST", keks: k.gast, rumpf: { text: "und jetzt?" },
|
||
});
|
||
console.log(` ... und der Server: ${serverSagt.code}/${serverSagt.j?.fehler}`);
|
||
if (!beimGast.gesperrt || !beimGast.grund.includes("stumm")
|
||
|| serverSagt.j?.fehler !== "stumm_geschaltet") {
|
||
console.log(" ACHTUNG: Die Stummschaltung kommt nicht oder mit falschem Grund an.");
|
||
ausgang = 1;
|
||
}
|
||
await roh(`/workspace/api/reaktion/massnahme/${IDS.gast}`, { method: "DELETE", keks: k.admin });
|
||
await modi.seite.waitForTimeout(900);
|
||
|
||
/* MELDEN -- der Weg fuer die, die selbst nichts tun koennen. */
|
||
const letzter = await roh("/workspace/api/reaktion/chat", { keks: k.admin });
|
||
const zuMelden = (letzter.j?.beitraege || []).filter((b) => b.person_id === IDS.gast2).pop();
|
||
if (zuMelden) {
|
||
await roh(`/workspace/api/reaktion/chat/${zuMelden.id}/melden`, {
|
||
method: "POST", keks: k.gast, rumpf: { grund: "unfreundlich" },
|
||
});
|
||
}
|
||
await modi.seite.waitForTimeout(1800);
|
||
const chip = await modi.seite.evaluate(() => {
|
||
const k2 = document.getElementById("chat-meldungen");
|
||
if (!k2) return null;
|
||
const r = k2.getBoundingClientRect();
|
||
return { versteckt: !!k2.hidden, wort: k2.textContent.trim(), hoch: Math.round(r.height) };
|
||
});
|
||
console.log(` Meldungs-Zeichen: „${chip?.wort}" (versteckt=${chip?.versteckt}, `
|
||
+ `${chip?.hoch} px hoch)`);
|
||
if (!chip || chip.versteckt || !/Meldung/.test(chip.wort)) {
|
||
console.log(" ACHTUNG: Die Meldung erreicht die Moderation nicht.");
|
||
ausgang = 1;
|
||
}
|
||
await modi.seite.click("#chat-meldungen").catch(() => {});
|
||
await modi.seite.waitForTimeout(400);
|
||
const liste = await modi.seite.evaluate(() => {
|
||
const m = document.querySelector(".modmenue--liste");
|
||
if (!m) return null;
|
||
const r = m.getBoundingClientRect();
|
||
const knoepfe = [...m.querySelectorAll(".modmenue__knopf")];
|
||
const greifbar = knoepfe.filter((k2) => {
|
||
const q = k2.getBoundingClientRect();
|
||
const oben = document.elementFromPoint(q.left + q.width / 2, q.top + q.height / 2);
|
||
return !!oben && (k2 === oben || k2.contains(oben));
|
||
}).length;
|
||
return {
|
||
zeilen: m.querySelectorAll(".modmeldung").length,
|
||
text: m.querySelector(".modmeldung__text")?.textContent || "",
|
||
knoepfe: knoepfe.length, greifbar,
|
||
rechts: Math.round(r.right), unten: Math.round(r.bottom),
|
||
fensterBreit: window.innerWidth, fensterHoch: window.innerHeight,
|
||
};
|
||
});
|
||
if (!liste || liste.zeilen < 1) {
|
||
console.log(" ACHTUNG: Die Meldungsliste geht nicht auf.");
|
||
ausgang = 1;
|
||
} else {
|
||
console.log(` ${liste.zeilen} Meldung(en), erste: „${liste.text}" -- `
|
||
+ `${liste.greifbar}/${liste.knoepfe} Knoepfe anfassbar`);
|
||
if (liste.rechts > liste.fensterBreit || liste.unten > liste.fensterHoch) {
|
||
console.log(" ACHTUNG: Die Meldungsliste steht aus dem Bild heraus.");
|
||
ausgang = 1;
|
||
}
|
||
if (liste.greifbar !== liste.knoepfe) {
|
||
console.log(" ACHTUNG: Ein Knopf der Meldungsliste ist verdeckt.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
await bild(modi.seite, "13-meldungen");
|
||
if (modi.konsole.length) console.log(" KONSOLE:", modi.konsole.slice(0, 4).join(" | "));
|
||
|
||
/* AUS DER SENDUNG -- der haerteste Fall. Wer rausgenommen wird, muss
|
||
es ERFAHREN; vorher lief bei ihm das Video weiter, und nur die
|
||
Anwesenheitsmeldung schlug still fehl. */
|
||
await roh("/workspace/api/reaktion/massnahme", {
|
||
method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast, art: "raus" },
|
||
});
|
||
await zuschauer.seite.waitForTimeout(2500);
|
||
const rausgesetzt = await zuschauer.seite.evaluate(() => {
|
||
const k2 = document.querySelector(".raus");
|
||
return {
|
||
da: !!k2, markiert: document.body.dataset.raus === "ja",
|
||
titel: k2?.querySelector(".raus__titel")?.textContent || "",
|
||
deckt: k2 ? Math.round(k2.getBoundingClientRect().height) : 0,
|
||
fensterHoch: window.innerHeight,
|
||
};
|
||
});
|
||
console.log(` Die Rausgenommene sieht: „${rausgesetzt.titel}" `
|
||
+ `(${rausgesetzt.deckt} von ${rausgesetzt.fensterHoch} px)`);
|
||
if (!rausgesetzt.da || !rausgesetzt.markiert
|
||
|| rausgesetzt.deckt < rausgesetzt.fensterHoch - 2) {
|
||
console.log(" ACHTUNG: Wer rausgenommen wird, merkt es nicht.");
|
||
ausgang = 1;
|
||
}
|
||
await bild(zuschauer.seite, "14-aus-der-sendung");
|
||
await roh(`/workspace/api/reaktion/massnahme/${IDS.gast}`, { method: "DELETE", keks: k.admin });
|
||
|
||
await modi.kontext.close();
|
||
await host.kontext.close();
|
||
await gast2.kontext.close();
|
||
await zuschauer.kontext.close();
|
||
|
||
{
|
||
/* ==== DIE REGIELEISTE AM HANDY ==================================
|
||
Sieben Register auf 390 Pixel passen nicht nebeneinander -- sie
|
||
muss rollen. Gemessen wird beides: dass sie rollt, und dass sie
|
||
trotzdem nur EINE Zeile hoch ist. Umbrechen waere die stille
|
||
Alternative, und die kostet das Video 44 Pixel. */
|
||
{
|
||
const chef = await alsWer("admin", 390, 844);
|
||
await chef.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "domcontentloaded" });
|
||
await chef.seite.waitForTimeout(2600);
|
||
const l = await chef.seite.evaluate(() => {
|
||
const r = document.getElementById("reiter");
|
||
const le = document.getElementById("regieleiste");
|
||
if (!r || le.hidden) return null;
|
||
const k = [...r.querySelectorAll(".reiter__knopf")];
|
||
const rr = r.getBoundingClientRect();
|
||
return {
|
||
hoch: Math.round(le.getBoundingClientRect().height),
|
||
zeilen: new Set(k.map((x) => Math.round(x.getBoundingClientRect().top))).size,
|
||
rollt: r.scrollWidth > r.clientWidth + 1,
|
||
sichtbar: k.filter((x) => {
|
||
const q = x.getBoundingClientRect();
|
||
return q.left >= rr.left - 1 && q.right <= rr.right + 1;
|
||
}).length,
|
||
schild: !!document.querySelector(".regieleiste__schild")?.offsetParent,
|
||
ueber: Math.max(0, document.documentElement.scrollWidth
|
||
- document.documentElement.clientWidth),
|
||
/* WER LIEGT AM WEITESTEN RECHTS? Ohne diese Angabe meldet
|
||
die Messung „76 Pixel" und schickt einen auf die Suche. */
|
||
wer: (() => {
|
||
const kante = document.documentElement.clientWidth;
|
||
const raus = [];
|
||
for (const e of document.querySelectorAll("*")) {
|
||
const q = e.getBoundingClientRect();
|
||
if (q.width === 0 || q.right <= kante + 1) continue;
|
||
raus.push(`${e.tagName.toLowerCase()}${e.id ? "#" + e.id : ""}`
|
||
+ `${typeof e.className === "string" && e.className
|
||
? "." + e.className.trim().split(/\s+/)[0] : ""}`
|
||
+ `:${Math.round(q.left)}..${Math.round(q.right)}`);
|
||
}
|
||
return raus.slice(0, 6).join(" | ") || "(nichts)";
|
||
})(),
|
||
};
|
||
});
|
||
if (!l) {
|
||
console.log(" ACHTUNG: Am Handy gibt es die Regieleiste nicht.");
|
||
ausgang = 1;
|
||
} else {
|
||
console.log(` Handy-Regie: ${l.hoch} px, ${l.zeilen} Zeile(n), rollt=${l.rollt}, `
|
||
+ `${l.sichtbar} von 7 im Bild, Schild=${l.schild}, seitlich ${l.ueber} px`);
|
||
if (l.ueber > 0) console.log(` ueber die Kante: ${l.wer}`);
|
||
/* SIE BRICHT UM, SEIT DEM 28.09.2026 -- UND DAS IST DER SINN.
|
||
Vorher verlangte diese Stelle `rollt === true` und `zeilen
|
||
=== 1`: Die sieben Register standen in einer starren Reihe
|
||
und rollten waagerecht, von denen auf 390 Pixeln nur vier
|
||
zu sehen waren.
|
||
|
||
Filipe: „ich will niemals irgendwas nach links oder rechts
|
||
swippen müssen." Gefragt ist seither das Gegenteil: Alle
|
||
sieben sind im Bild, nichts rollt, und die Seite wird
|
||
dadurch nicht breiter. */
|
||
if (l.rollt || l.sichtbar !== 7 || l.ueber > 0 || l.schild) {
|
||
console.log(` ACHTUNG: Die Regieleiste am Handy rollt (${l.rollt}), `
|
||
+ `zeigt nur ${l.sichtbar} von 7 Registern oder schiebt die Seite `
|
||
+ `${l.ueber} px breiter.`);
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
/* WAS BLEIBT VOM BILD, WENN DIE REGIE OFFEN IST?
|
||
Am Handy ist das die eigentliche Frage. Ein Kinosaal von 131
|
||
Pixeln ist kein Bild mehr, sondern ein Streifen -- und man
|
||
merkt es erst, wenn man mitten in der Sendung etwas einstellen
|
||
will und nicht mehr sieht, worueber man redet. */
|
||
const platzHandy = await chef.seite.evaluate(() => {
|
||
const g = (id) => Math.round(document.getElementById(id).getBoundingClientRect().height);
|
||
const alle = [...document.getElementById("saal").children]
|
||
.filter((e) => e.getBoundingClientRect().height > 0)
|
||
.map((e) => `${e.id || e.className.split(" ")[0]}=${Math.round(e.getBoundingClientRect().height)}`);
|
||
/* WIEVIEL VOM BILD IST WIRKLICH ZU SEHEN?
|
||
Die Hoehe der Kinozeile sagt das nicht: Seit die Regie am
|
||
Handy eine Schublade darueber ist, kann die Zeile 431 Pixel
|
||
hoch sein und trotzdem vollstaendig verdeckt. Ein gruener
|
||
Haken ueber eine Zeilenhoehe waere genau die Sorte, die das
|
||
Falsche misst. Gefragt ist der Streifen, der oberhalb der
|
||
Schublade uebrig bleibt. */
|
||
const kino = document.getElementById("teil-live").getBoundingClientRect();
|
||
const regie = document.getElementById("pult").getBoundingClientRect();
|
||
const frei = Math.max(0, Math.round(Math.min(kino.bottom, regie.top) - kino.top));
|
||
return { saal: g("saal"), leiste: g("regieleiste"), kino: g("teil-live"), pult: g("pult"),
|
||
frei, alle: alle.join(" + ") };
|
||
});
|
||
console.log(` Handy offen: Saal ${platzHandy.saal} = Leiste ${platzHandy.leiste} `
|
||
+ `+ Kino ${platzHandy.kino} + Regie ${platzHandy.pult}`);
|
||
console.log(` Handy Zeilen: ${platzHandy.alle}`);
|
||
/* UEBERLAPPEN SICH DIE KNOEPFE DER LEISTE?
|
||
`pruef-handy` fragt nur, ob die MITTE eines Knopfes frei ist
|
||
-- zwei Knoepfe koennen sich an den Raendern ueberlagern und
|
||
trotzdem beide „erreichbar" sein. Auf dem Bildschirmfoto
|
||
sieht das aus wie ein Fehler, und wer danebentippt, trifft
|
||
den falschen. */
|
||
const gedraengt = await chef.seite.evaluate(() => {
|
||
const knoepfe = [...document.querySelectorAll(".transport button, .transport input")]
|
||
.map((e) => ({ e, r: e.getBoundingClientRect() }))
|
||
.filter((x) => x.r.width > 0 && x.r.height > 0);
|
||
const raus = [];
|
||
for (let i = 0; i < knoepfe.length; i++) {
|
||
for (let j = i + 1; j < knoepfe.length; j++) {
|
||
const a = knoepfe[i].r, b = knoepfe[j].r;
|
||
const ueber = Math.min(a.right, b.right) - Math.max(a.left, b.left);
|
||
const hoch = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
|
||
if (ueber > 1 && hoch > 1) {
|
||
const n = (x) => x.e.id || x.e.className.split(" ")[0] || x.e.tagName;
|
||
raus.push(`${n(knoepfe[i])} / ${n(knoepfe[j])} (${Math.round(ueber)}x${Math.round(hoch)} px)`);
|
||
}
|
||
}
|
||
}
|
||
return raus;
|
||
});
|
||
if (gedraengt.length) {
|
||
console.log(` ACHTUNG: ${gedraengt.length} Knopfpaar(e) in der Leiste `
|
||
+ `ueberlappen sich: ${gedraengt.slice(0, 4).join(", ")}`);
|
||
ausgang = 1;
|
||
} else {
|
||
console.log(" Kein Knopf der Transportleiste liegt ueber einem anderen.");
|
||
}
|
||
const spur = await chef.seite.evaluate(() => {
|
||
const saal = document.getElementById("saal");
|
||
const r = saal.getBoundingClientRect();
|
||
const teile = [...saal.children].filter((e) => e.getBoundingClientRect().height > 0)
|
||
.map((e) => {
|
||
const q = e.getBoundingClientRect();
|
||
return `${e.id || e.className.split(" ")[0]} ${Math.round(q.top)}..${Math.round(q.bottom)}`;
|
||
});
|
||
const tr = document.querySelector(".transport");
|
||
const teileT = [...(tr ? tr.children : [])].map((e) => {
|
||
const q = e.getBoundingClientRect();
|
||
return `${e.className.split(" ")[0]}=${Math.round(q.height)}`;
|
||
});
|
||
return `Saal ${Math.round(r.top)}..${Math.round(r.bottom)} (Fenster ${window.innerHeight})`
|
||
+ ` | Zeilen ${getComputedStyle(saal).gridTemplateRows}`
|
||
+ ` | ${teile.join(" / ")}`
|
||
+ ` | Transport: ${teileT.join(" + ")}`
|
||
+ ` | Regie-Kopf: ${(() => {
|
||
const l = document.querySelector(".pult__leiste");
|
||
if (!l) return "-";
|
||
const kinder = [...l.children].map((e) => {
|
||
const q = e.getBoundingClientRect();
|
||
return `${e.className.split(" ")[0] || e.tagName.toLowerCase()}`
|
||
+ ` ${Math.round(q.top)}..${Math.round(q.bottom)}`;
|
||
});
|
||
return `${Math.round(l.getBoundingClientRect().height)} px = ${kinder.join(" / ")}`;
|
||
})()}`;
|
||
});
|
||
console.log(` SPUR: ${spur}`);
|
||
console.log(` Vom Bild sichtbar (nicht von der Regie verdeckt): ${platzHandy.frei} px`);
|
||
if (platzHandy.frei < 120) {
|
||
offenePunkte.push(`Am Handy bleibt bei offener Regie vom Bild fast nichts `
|
||
+ `(${platzHandy.frei} px sichtbar, noetig 120). Bekannt seit 28.09.2026. `
|
||
+ `Wurzel: die umgebrochene Transportleiste braucht auf 320 px `
|
||
+ `Breite 220 Pixel. Der Weg steht in reaktion.css beim Handy-Block.`);
|
||
}
|
||
await bild(chef.seite, "21-regieleiste-handy");
|
||
await chef.kontext.close();
|
||
}
|
||
|
||
const handy = await alsWer("gast2", 390, 844);
|
||
await handy.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" });
|
||
await handy.seite.waitForTimeout(4000);
|
||
await vermessen(handy.seite, "Handy");
|
||
await bild(handy.seite, "5-live-handy");
|
||
|
||
await handy.seite.waitForTimeout(500);
|
||
await bild(handy.seite, "6-live-handy-chat");
|
||
const hm = await handy.seite.evaluate(() => {
|
||
const g = (w) => { const e = document.querySelector(w); if (!e) return "(fehlt)";
|
||
const r = e.getBoundingClientRect(); const c = getComputedStyle(e);
|
||
return `${Math.round(r.width)}x${Math.round(r.height)} display=${c.display}`; };
|
||
return { saal: g("#saal"), kino: g("#teil-live"), leinwand: g(".leinwand"),
|
||
schiene: g(".schiene"), schieneWert: document.getElementById("teil-live")?.dataset.schiene };
|
||
});
|
||
console.log(" Handy-Masse:", JSON.stringify(hm));
|
||
if (handy.konsole.length) console.log(" KONSOLE:", handy.konsole.slice(0, 4).join(" | "));
|
||
}
|
||
|
||
await browser.close();
|
||
await new Promise((r) => vorbau.close(r));
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
if (offenePunkte.length) {
|
||
console.log(`\n=== ${offenePunkte.length} bekannte(r) offene(r) Punkt(e) ===`);
|
||
for (const z of offenePunkte) console.log(` OFFEN: ${z}`);
|
||
}
|
||
console.log("\nFertig.");
|
||
process.exit(ausgang);
|