Filipe: „die videos muessen perfekt laufen wenn ich die reaktions
mache."
GEMESSEN, NICHT VERMUTET. mess-reaktion hat einen neuen Abschnitt:
Er liest den Spielstand am ECHTEN <video>-Element in beiden Browsern,
rechnet beide auf denselben Augenblick um und sagt, wie weit Host und
Zuschauerin auseinanderliegen. Die vorhandene Pruefung fragte nur, ob
Start und Stopp ANKOMMEN -- zwei Videos koennen beide laufen und
trotzdem zwanzig Sekunden auseinander sein.
vorher nachher
Im Lauf 0,12 s -0,04 s
Wer dazukommt 6,91 s 0,26 s
Uhr des Geraets 30 s vor -29,97 s 0,26 s
DER SCHLIMMSTE FUND: DIE UHR DES TELEFONS
Der Zuschauer rechnete `Date.now() - stand.gesendet`. `gesendet` kam
vom SERVER, `Date.now()` vom Geraet -- zwei Uhren, die nie jemand
verglichen hat. Wessen Telefon dreissig Sekunden vorgeht, schaute
29,97 s weiter als alle anderen. Kein langsames Netz, kein schwaches
Handy. Oertlich faellt das nie auf: Auf einem Rechner sind beide
Uhren dieselbe.
Jetzt geht ein ALTER hinaus statt einer Uhrzeit -- die Differenz
zweier SERVERzeiten. Der Empfaenger braucht dafuer gar keine Uhr,
nur eine Stoppuhr (`performance.now()`), und die haelt Zeitumstellung
und Aufwachen aus dem Ruhezustand aus.
WEITER
* WER DAZUKOMMT, STEIGT RICHTIG EIN. Der gespeicherte Stand war so
alt wie der letzte Takt des Hosts; neue Spalte `sekunde_am` sagt,
wann er galt.
* DAS TEMPO GEHT IN DIE HOCHRECHNUNG EIN. Bei 2x laeuft die Videouhr
doppelt so schnell -- das fehlte ganz.
* JEDER RECHNET ALLE ZWEI SEKUNDEN SELBST NACH statt nur auf Zuruf.
Wer stockte, hing bis zu fuenf Sekunden hinterher.
* DER VORLAUF NACH EINEM SPRUNG WIRD GEREGELT, nicht geschaetzt --
gemessen wird der Rest, nicht die Ladezeit (siehe unten).
* PUFFERN IST AUCH IN DER TRANSPORTLEISTE KEINE PAUSE. Der
Start-Stopp-Knopf rief beim Puffern `playVideo()` -- wer Pause
drueckte, bekam „weiter". Lampe und Knopfbild flackerten bei jedem
Stocken. Die Lektion vom 29.09. war nur an einer Stelle eingeloest.
* DAS TEMPO MELDET UEBER `hostMelden()` statt ein zweites Mal von
Hand -- es hielt Puffern fuer Stillstand und hielt damit beim
Umschalten alle an.
* EIN VIDEO-RUNDRUF STATT DREI ABSCHRIFTEN. Beim Einbau hatte ich
zuerst nur eine nachgezogen; das Tauschen der beiden Videos haette
still die alte Rechnung weiterverschickt.
DREI EIGENE FEHLER UNTERWEGS, alle im Text festgehalten
1. ZWEI FUNKTIONEN HIESSEN `springen`. Meine „spring AUF die Stelle"
wurde von der vorhandenen „spring UM so viele Sekunden"
ueberschrieben -- lautlos, rueckwirkend fuer die ganze Datei. Ein
Zuschauer bei Sekunde 100, dem „geh auf 101" gesagt wird, waere
bei 201 gelandet. DREI BROWSERLAEUFE HABEN ES NICHT GEFUNDEN:
oertlich blieb der Abstand unter der Sprungschwelle, die Zeile
lief kein einziges Mal. Gefunden hat es erst eine Frage an die
ganze Datei -- die steht jetzt als Pruefung drin und wurde
nachgestellt (rot).
2. DER VORLAUF LERNTE NUR NACH OBEN. Gemessen wurde die Ladezeit
nach einem Sprung; ein Sprung in geladenes Material dauert aber
gar nicht, meldet nichts und lehrt nichts. Ergebnis: -0,95 s
stabil. Jetzt wird das ERGEBNIS geregelt statt der Ursache.
3. DER AUSREISSER VON 39,84 s WAR MEINE EIGENE MESSUNG -- der Block
davor schickt eine erfundene Position. Jetzt wird abgeklungen und
in Messreihenfolge ausgegeben.
GEPRUEFT
pruef-reaktion 421/0 (14 neue, darunter der Riegel gegen die
Rueckkehr der Uhrenrechnung und die Wache gegen doppelte
Funktionsnamen -- beide nachgestellt), mess-reaktion ohne ein
einziges ACHTUNG, pruef-buehne 38/0, pruef-struktur, pruef-
zwischenspeicher 34/0.
Schemaaenderung: ADD COLUMN sekunde_am. Datenbank vorher gesichert
und geprueft (integrity_check ok, 20 Personen).
Co-Authored-By: Claude Opus 5 <[email protected]>
3539 lines
159 KiB
JavaScript
3539 lines
159 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;
|
||
}
|
||
|
||
/* =====================================================================
|
||
HAENGT DAS VIDEO? -- der Host darf sich nicht selbst anhalten
|
||
===================================================================== */
|
||
console.log("");
|
||
console.log("=== Der Gleichlauf des Videos ===");
|
||
{
|
||
/* Wie ein Pufferer aussieht, wenn er gemeldet wird: `laeuft:false`
|
||
bei einem Spieler, der in Wahrheit laeuft. Genau das schickt
|
||
`hostMelden()` im YouTube-Zustand 3. */
|
||
const pufferer = (seite) => seite.evaluate(async () => {
|
||
const stelle = document.getElementById("video-platz")?.dataset.spieler;
|
||
const a = await fetch("/workspace/api/reaktion/video", {
|
||
method: "POST", credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ laeuft: false, sekunde: 7 }),
|
||
});
|
||
return { vorher: stelle, code: a.status };
|
||
});
|
||
|
||
const stand = (seite) => seite.evaluate(
|
||
() => document.getElementById("video-platz")?.dataset.spieler || "(nichts)");
|
||
|
||
/* Erst sicherstellen, dass ueberhaupt etwas laeuft -- sonst misst
|
||
die Pruefung ein stehendes Video und nennt das Erfolg. */
|
||
const vorHost = await stand(host.seite);
|
||
const vorZu = await stand(zuschauer.seite);
|
||
console.log(` Vorher: Host=${vorHost} Zuschauerin=${vorZu}`);
|
||
const laeuftVorher = (w) => w === "laeuft" || w === "puffert";
|
||
if (!laeuftVorher(vorHost) || !laeuftVorher(vorZu)) {
|
||
console.log(" ACHTUNG: Vor der Messung laeuft schon nichts -- "
|
||
+ "die Frage laesst sich so nicht beantworten.");
|
||
ausgang = 1;
|
||
} else {
|
||
const p = await pufferer(host.seite);
|
||
console.log(` Ein Pufferer wird gemeldet: ${p.code}`);
|
||
await host.seite.waitForTimeout(2000);
|
||
await zuschauer.seite.waitForTimeout(500);
|
||
|
||
const nachHost = await stand(host.seite);
|
||
const nachZu = await stand(zuschauer.seite);
|
||
console.log(` Danach: Host=${nachHost} Zuschauerin=${nachZu}`);
|
||
|
||
/* DER BEFUND. Der Host hat nichts angehalten -- er hat nur
|
||
gemeldet, dass gerade Daten fehlen. Steht sein Video danach,
|
||
hat sein eigenes Echo es angehalten. */
|
||
if (!laeuftVorher(nachHost)) {
|
||
console.log(" ACHTUNG: Der Host haelt sich selbst an. Ein kurzer "
|
||
+ "Pufferer meldet 'laeuft nicht', das Echo kommt zurueck und "
|
||
+ "drueckt Pause -- deshalb haengt das Video.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ---- DIE GEGENPROBE: ein ECHTES Anhalten wirkt weiter ------
|
||
Ohne sie waere „der Host folgt sich nicht selbst" womoeglich
|
||
mit einem kaputten Gleichlauf fuer alle bezahlt.
|
||
|
||
GEDRUECKT WIRD DER KNOPF, nicht eine Meldung gefaelscht. Bei
|
||
einem echten Anhalten meldet der Host durchgehend „steht" --
|
||
die Antwort haengt damit nicht am Zufall seines
|
||
Fuenf-Sekunden-Taktes. Die erste Fassung dieser Gegenprobe hat
|
||
genau daran Alarm geschlagen: Sie hielt das Selbstheilen des
|
||
Systems fuer einen Fehler. */
|
||
/* GEWARTET WIRD AUF DEN ZUSTAND, NICHT AUF DIE UHR. Eine feste
|
||
Wartezeit stimmt meistens -- und wenn der Fuenf-Sekunden-Takt
|
||
des Hosts dazwischenfaellt, hinkt die Zuschauerin ein Ereignis
|
||
hinterher und die Messung meldet einen Fehler, den es nicht
|
||
gibt. Genau das ist einmal passiert. */
|
||
const warteAuf = async (seite, wert, frist = 9000) => {
|
||
try {
|
||
await seite.waitForFunction(
|
||
(w) => document.getElementById("video-platz")?.dataset.spieler === w,
|
||
wert, { timeout: frist });
|
||
return true;
|
||
} catch { return false; }
|
||
};
|
||
|
||
await host.seite.evaluate(() => document.getElementById("v-spiel")?.click());
|
||
await warteAuf(host.seite, "pause");
|
||
await warteAuf(zuschauer.seite, "pause");
|
||
const haltHost = await stand(host.seite);
|
||
const haltZu = await stand(zuschauer.seite);
|
||
console.log(` Host haelt wirklich an: Host=${haltHost} Zuschauerin=${haltZu}`);
|
||
if (haltHost === "laeuft" || haltZu === "laeuft") {
|
||
console.log(" ACHTUNG: Ein echtes Anhalten des Hosts kommt nicht an -- "
|
||
+ "dann laeuft niemand mehr synchron.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* Und wieder los, mit demselben Knopf. */
|
||
await host.seite.evaluate(() => document.getElementById("v-spiel")?.click());
|
||
await warteAuf(host.seite, "laeuft");
|
||
await warteAuf(zuschauer.seite, "laeuft");
|
||
const weiterHost = await stand(host.seite);
|
||
const weiterZu = await stand(zuschauer.seite);
|
||
console.log(` Und wieder los: Host=${weiterHost} Zuschauerin=${weiterZu}`);
|
||
if (!laeuftVorher(weiterHost) || !laeuftVorher(weiterZu)) {
|
||
console.log(" ACHTUNG: Nach dem Anhalten laeuft es nicht wieder an.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ---- DER KERN: was meldet der Host, WAEHREND er puffert? ----
|
||
Die beiden Teile darueber pruefen die Folge. Sie blieben auch
|
||
gruen, wenn der Host beim Puffern weiter „steht" meldete --
|
||
die Zuschauer ruckelten dann bei jedem Pufferer, und niemand
|
||
saehe es hier.
|
||
|
||
Puffern wird hergestellt: weit nach vorn springen, wohin noch
|
||
nichts geladen ist. Ob das GELINGT, ist nicht sicher -- auf
|
||
einer schnellen Leitung kann der Sprung fertig sein, bevor
|
||
jemand hinsieht. Deshalb der dritte Ausgang. */
|
||
let gesehen = null;
|
||
for (let i = 0; i < 8 && !gesehen; i++) {
|
||
await host.seite.evaluate(() => {
|
||
for (let n = 0; n < 12; n++) document.getElementById("v-vor")?.click();
|
||
});
|
||
for (let t = 0; t < 14 && !gesehen; t++) {
|
||
const w = await host.seite.evaluate(() => ({
|
||
spieler: document.getElementById("video-platz")?.dataset.spieler,
|
||
}));
|
||
if (w.spieler === "puffert") {
|
||
/* SOFORT NACHSEHEN, was der Server jetzt weiss. Eine
|
||
Sekunde spaeter ist der Pufferer vorbei und die Antwort
|
||
nichts mehr wert. */
|
||
gesehen = await host.seite.evaluate(async () => {
|
||
const a = await fetch("/workspace/api/reaktion", { credentials: "same-origin" });
|
||
const j = await a.json();
|
||
return { laeuft: j.laeuft, stand: document.getElementById("video-platz")?.dataset.spieler };
|
||
});
|
||
}
|
||
await host.seite.waitForTimeout(120);
|
||
}
|
||
}
|
||
if (!gesehen) {
|
||
console.log(" HINWEIS: Ein Pufferer war hier nicht herzustellen "
|
||
+ "(schnelle Leitung) -- was der Host dabei meldet, konnte ich "
|
||
+ "nicht nachsehen.");
|
||
offenePunkte.push("Was der Host beim Puffern meldet, war in diesem "
|
||
+ "Lauf nicht messbar -- der Sprung war zu schnell geladen.");
|
||
} else {
|
||
console.log(` Beim Puffern meldet der Server: laeuft=${gesehen.laeuft} `
|
||
+ `(Spieler: ${gesehen.stand})`);
|
||
if (!gesehen.laeuft) {
|
||
console.log(" ACHTUNG: Der Host meldet beim Puffern 'laeuft nicht'. "
|
||
+ "Dann bekommt jeder Zuschauer bei jedem Stocken der Hostleitung "
|
||
+ "ein Pause-Play-Paar -- das ist das Ruckeln.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* Und wieder anlaufen lassen, damit die folgenden Messungen ein
|
||
laufendes Video vorfinden. */
|
||
await host.seite.evaluate(() => fetch("/workspace/api/reaktion/video", {
|
||
method: "POST", credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ laeuft: true, sekunde: 7 }),
|
||
}));
|
||
await host.seite.waitForTimeout(1200);
|
||
}
|
||
}
|
||
|
||
/* ==== WIE WEIT LIEGEN SIE AUSEINANDER? ==============================
|
||
|
||
Der Abschnitt darueber prueft, ob Start und Stopp ANKOMMEN. Das ist
|
||
etwas anderes als gemeinsam schauen: Zwei Videos koennen beide
|
||
laufen und trotzdem zwanzig Sekunden auseinanderliegen. Dort saehe
|
||
man davon nichts -- beide meldeten brav „laeuft".
|
||
|
||
GELESEN WIRD AM ECHTEN <video>-ELEMENT im YouTube-Rahmen. Nicht an
|
||
einer Zahl, die die Seite ueber sich selbst schreibt: Die koennte
|
||
stimmen, waehrend das Bild etwas anderes zeigt. Playwright kommt an
|
||
den Rahmen heran, also wird dort gemessen, wo das Bild entsteht.
|
||
|
||
BEIDE AUF DENSELBEN AUGENBLICK UMGERECHNET. Zwei Ablesungen
|
||
nacheinander liegen 20 bis 80 ms auseinander; ungerechnet waere
|
||
genau das der „Abstand". Jede Ablesung bekommt deshalb ihren
|
||
Zeitpunkt (die Mitte zwischen Anfrage und Antwort) und wird auf
|
||
einen gemeinsamen Bezug hochgerechnet -- mit dem Tempo, denn bei 2x
|
||
laeuft die Uhr im Video doppelt so schnell.
|
||
==================================================================== */
|
||
console.log("");
|
||
console.log(" --- Der Abstand zwischen Host und Zuschauerin ---");
|
||
{
|
||
const stelleLesen = async (w) => {
|
||
const f = w.seite.frames().find((x) => /youtube(-nocookie)?\.com\/embed/.test(x.url()));
|
||
if (!f) return null;
|
||
const t0 = Date.now();
|
||
let z = null;
|
||
try {
|
||
z = await f.evaluate(() => {
|
||
const v = document.querySelector("video");
|
||
return v ? { zeit: v.currentTime, steht: v.paused, tempo: v.playbackRate || 1 } : null;
|
||
});
|
||
} catch { return null; } /* der Rahmen wurde gerade neu gebaut */
|
||
const t1 = Date.now();
|
||
return z ? { ...z, am: (t0 + t1) / 2, spanne: (t1 - t0) / 1000 } : null;
|
||
};
|
||
|
||
/** Der Abstand in Sekunden. Positiv heisst: die Zuschauerin ist
|
||
* HINTEN -- der Host ist schon weiter. */
|
||
const abstandMessen = async () => {
|
||
const [h, g] = await Promise.all([stelleLesen(host), stelleLesen(zuschauer)]);
|
||
if (!h || !g) return null;
|
||
const bezug = Math.max(h.am, g.am);
|
||
const bei = (z) => z.zeit + (z.steht ? 0 : ((bezug - z.am) / 1000) * z.tempo);
|
||
return {
|
||
abstand: bei(h) - bei(g),
|
||
/* Wie genau diese eine Messung ueberhaupt sein kann. Ein
|
||
Befund, der kleiner ist als seine eigene Ungenauigkeit, ist
|
||
keiner. */
|
||
genauigkeit: h.spanne + g.spanne,
|
||
hSteht: h.steht, gSteht: g.steht,
|
||
};
|
||
};
|
||
|
||
const reihe = async (wieoft, abstandMs) => {
|
||
const werte = [];
|
||
for (let i = 0; i < wieoft; i++) {
|
||
const m = await abstandMessen();
|
||
if (m) werte.push(m);
|
||
await host.seite.waitForTimeout(abstandMs);
|
||
}
|
||
return werte;
|
||
};
|
||
|
||
const auswerten = (werte, was) => {
|
||
if (!werte.length) {
|
||
console.log(` KONNTE NICHT NACHSEHEN: ${was} -- der YouTube-Rahmen `
|
||
+ "war nicht zu lesen.");
|
||
offenePunkte.push(`Abstand „${was}" war in diesem Lauf nicht messbar.`);
|
||
return null;
|
||
}
|
||
const b = werte.map((w) => w.abstand).sort((x, y) => x - y);
|
||
const mitte = b[Math.floor(b.length / 2)];
|
||
const groesste = b.reduce((a, x) => (Math.abs(x) > Math.abs(a) ? x : a), 0);
|
||
const gen = Math.max(...werte.map((w) => w.genauigkeit));
|
||
console.log(` ${was}: Mitte ${mitte.toFixed(2)} s, `
|
||
+ `groesster ${groesste.toFixed(2)} s, ${werte.length} Messungen `
|
||
+ `(Messgenauigkeit ${gen.toFixed(2)} s)`);
|
||
/* IN MESSREIHENFOLGE, nicht sortiert. Die Reihenfolge ist eine
|
||
Auskunft: Ein Ausreisser gleich am Anfang heisst „es hat sich
|
||
noch nicht eingerenkt", einer mittendrin heisst etwas ganz
|
||
anderes. Sortiert sieht beides gleich aus. */
|
||
console.log(` der Reihe nach: `
|
||
+ `${werte.map((w) => w.abstand.toFixed(2)).join(" ")}`);
|
||
return { mitte, groesste };
|
||
};
|
||
|
||
/* ---- 1. Der Normalfall: beide laufen schon eine Weile ---------
|
||
|
||
ERST ABKLINGEN LASSEN -- und zwar wegen des Blocks DARUEBER,
|
||
nicht wegen des Programms. Um einen Pufferer herzustellen,
|
||
springt der Host dort 60 Sekunden vor; danach schickt die
|
||
Messung eine erfundene Meldung („die Sendung steht bei Sekunde
|
||
7"). Die Zuschauerin folgt ihr, der Host bleibt bei 60 -- bis
|
||
sein naechster Takt die Wahrheit sagt.
|
||
|
||
Im ersten Lauf stand deshalb ein Ausreisser von 39,84 s in der
|
||
Reihe. Er kam aus dem Aufbau der Messung, nicht aus der
|
||
Software. Zwei Takte des Hosts abwarten, dann ist der Zustand
|
||
wieder echt. */
|
||
await host.seite.waitForTimeout(11000);
|
||
const normal = auswerten(await reihe(8, 1800), "Im Lauf");
|
||
if (normal && Math.abs(normal.mitte) > 1.5) {
|
||
console.log(" ACHTUNG: Sie schauen nicht dasselbe. Ein Abstand von "
|
||
+ `${normal.mitte.toFixed(1)} s heisst, dass die Zuschauerin eine andere `
|
||
+ "Stelle sieht als der Host -- bei einer Reaction ist genau das der Punkt.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ---- 2. Wer mitten in der Sendung dazukommt -------------------
|
||
Der haeufigste Fall ueberhaupt: Jemand macht die Seite auf,
|
||
waehrend schon laeuft. Er bekommt den Stand aus der Datenbank --
|
||
und der ist so alt wie der letzte Takt des Hosts. */
|
||
await zuschauer.seite.reload({ waitUntil: "networkidle" });
|
||
let da = false;
|
||
for (let i = 0; i < 40 && !da; i++) {
|
||
da = !!(await stelleLesen(zuschauer));
|
||
if (!da) await zuschauer.seite.waitForTimeout(250);
|
||
}
|
||
if (!da) {
|
||
console.log(" KONNTE NICHT NACHSEHEN: nach dem Neuladen kam kein Spieler.");
|
||
offenePunkte.push("Der Abstand beim Dazukommen war nicht messbar.");
|
||
} else {
|
||
/* SOFORT messen, nicht nach zehn Sekunden -- die Frage ist, was
|
||
der Mensch in der ersten Sekunde sieht. Danach noch einmal:
|
||
Holt das System den Rueckstand von allein auf? */
|
||
const gleich = await abstandMessen();
|
||
if (gleich) {
|
||
console.log(` Beim Dazukommen: ${gleich.abstand.toFixed(2)} s`);
|
||
if (Math.abs(gleich.abstand) > 2) {
|
||
console.log(" ACHTUNG: Wer dazukommt, steigt "
|
||
+ `${gleich.abstand.toFixed(1)} s versetzt ein.`);
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
await zuschauer.seite.waitForTimeout(7000);
|
||
const spaeter = await abstandMessen();
|
||
if (spaeter) {
|
||
console.log(` Sieben Sekunden spaeter: ${spaeter.abstand.toFixed(2)} s`);
|
||
}
|
||
}
|
||
|
||
/* ---- 3. UND DIE UHR DES GERAETS -------------------------------
|
||
Der Zuschauer rechnet `Date.now() - stand.gesendet`. `gesendet`
|
||
kommt vom SERVER, `Date.now()` vom Handy. Das sind zwei Uhren,
|
||
und niemand hat sie je verglichen.
|
||
|
||
HERGESTELLT, NICHT ABGEWARTET: Die Uhr der Seite wird um 30
|
||
Sekunden vorgestellt -- genau das, was ein Telefon mit von Hand
|
||
gestellter Zeit mitbringt. Ein Geraet, das nachgeht, faellt
|
||
durch `Math.max(0, ...)` weniger auf; eines, das vorgeht, nicht. */
|
||
console.log("");
|
||
console.log(" --- Wenn die Uhr des Geraets falsch geht ---");
|
||
await zuschauer.seite.evaluate((ms) => {
|
||
window.__echtJetzt = Date.now;
|
||
Date.now = () => window.__echtJetzt() + ms;
|
||
}, 30000);
|
||
/* Zwei Takte des Hosts abwarten -- erst dann hat die falsche Uhr
|
||
ueberhaupt Gelegenheit, sich auszuwirken. */
|
||
await host.seite.waitForTimeout(12000);
|
||
const schief = auswerten(await reihe(5, 1500), "Uhr 30 s vor");
|
||
if (schief && Math.abs(schief.mitte) > 3) {
|
||
console.log(" ACHTUNG: Eine falsch gehende Uhr im Geraet verschiebt das "
|
||
+ `Video um ${schief.mitte.toFixed(1)} s. Der Abstand wird aus zwei `
|
||
+ "Uhren gerechnet, die niemand verglichen hat -- wessen Telefon "
|
||
+ "falsch steht, schaut etwas anderes.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* DIE GEGENPROBE ZUR GEGENPROBE: Uhr zurueckstellen, und es muss
|
||
sich wieder einrenken. Ohne sie hiesse ein grosser Wert oben
|
||
womoeglich nur, dass die Messung kaputtgegangen ist. */
|
||
await zuschauer.seite.evaluate(() => {
|
||
if (window.__echtJetzt) Date.now = window.__echtJetzt;
|
||
});
|
||
await host.seite.waitForTimeout(12000);
|
||
const wieder = auswerten(await reihe(4, 1200), "Uhr wieder richtig");
|
||
if (wieder && Math.abs(wieder.mitte) > 2) {
|
||
console.log(" ACHTUNG: Nach dem Zurueckstellen der Uhr renkt es sich "
|
||
+ "nicht wieder ein.");
|
||
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(() => {
|
||
/* SPUR: die ganze Kette vom Saal bis zur Leinwand. Wer nur die
|
||
Hoehe des Kinos kennt, raet, an welchem Glied sie verloren
|
||
geht. */
|
||
window.__spur = ["saal", "raum", "teil-live", "leinwand", "schiene"].map((id) => {
|
||
const e = document.getElementById(id);
|
||
if (!e) return `${id}=FEHLT`;
|
||
const r = e.getBoundingClientRect();
|
||
const c = getComputedStyle(e);
|
||
return `${id} ${Math.round(r.width)}x${Math.round(r.height)} `
|
||
+ `@${Math.round(r.left)},${Math.round(r.top)} ${c.display}`;
|
||
}).join(" | ");
|
||
const l = document.getElementById("regieleiste").getBoundingClientRect();
|
||
const k = document.getElementById("teil-live").getBoundingClientRect();
|
||
const s2 = document.getElementById("saal").getBoundingClientRect();
|
||
return {
|
||
spur: window.__spur,
|
||
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(` [Kette] ${platz.spur}`);
|
||
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 });
|
||
|
||
/* ---- UND HEBT SICH DER VORHANG WIEDER? (30.09.2026) --------------
|
||
Bis dahin nicht: `rausgenommen()` schliesst den Ereignisstrom --
|
||
mit Absicht, wer draussen ist soll nicht weiterschauen -- und
|
||
damit kam die Ruecknahme dort nie an. Wir haben die Leute gebeten,
|
||
neu zu laden.
|
||
|
||
Der offene Punkt stand seit dem 29.09. in der Projektnotiz und
|
||
hatte keine Pruefung. Genau so bleibt ein bekannter Mangel fuer
|
||
immer bekannt: Niemand merkt, wenn er behoben ist, und niemand
|
||
merkt, wenn er zurueckkommt.
|
||
|
||
Die Nachfrage hinter dem Vorhang laeuft alle 20 Sekunden; 35
|
||
lassen ihr eine Runde Luft. Gewartet wird auf das Ergebnis, nicht
|
||
die volle Zeit. */
|
||
{
|
||
let zurueck = true;
|
||
try {
|
||
await zuschauer.seite.waitForFunction(
|
||
() => document.body.dataset.raus !== "ja"
|
||
&& !document.querySelector(".raus__titel"),
|
||
null, { timeout: 35_000 });
|
||
} catch { zurueck = false; }
|
||
console.log(` Der Vorhang hebt sich von selbst: ${zurueck ? "ja" : "NEIN"}`);
|
||
if (!zurueck) {
|
||
console.log(" ACHTUNG: Wer zurueckgeholt wird, sitzt weiter vor "
|
||
+ "'Du bist aus der Sendung' und muss von Hand neu laden.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE WERBEEINBLENDUNG -- beim Zuschauer, nicht beim Host
|
||
===================================================================== */
|
||
console.log("");
|
||
console.log("=== Die Werbeeinblendung ===");
|
||
{
|
||
/* DER ZUSCHAUER WAR EBEN NOCH AUS DER SENDUNG GENOMMEN, und die
|
||
Massnahme ist zurueckgenommen -- der Vorhang steht trotzdem
|
||
noch. Das ist Absicht und kein Fehler: `rausgenommen()`
|
||
schliesst den Ereignisstrom, damit niemand die Sendung
|
||
weiterverfolgt, waehrend er draussen ist. Genau deshalb kommt
|
||
die Ruecknahme dort nicht mehr an.
|
||
|
||
Gemessen wird deshalb nach einem Neuladen -- das ist auch das,
|
||
was ein Mensch taete. Dass der Vorhang OHNE Neuladen stehen
|
||
bleibt, steht als offener Punkt in der Liste; hier still darum
|
||
herumzumessen waere das Verstecken eines Befunds. */
|
||
await zuschauer.seite.reload({ waitUntil: "domcontentloaded" });
|
||
/* GEWARTET WIRD AUF DEN STAND, NICHT AUF DIE SEITE. `#leinwand`
|
||
steht im HTML und ist sofort da; `body[data-stand]` setzt erst
|
||
`zeichnen()`, also nachdem der Stand angekommen UND verarbeitet
|
||
ist. Vorher hat diese Messung gemessen, waehrend die Seite noch
|
||
nichts wusste -- und das sah aus wie Flatterhaftigkeit. */
|
||
await zuschauer.seite.waitForFunction(
|
||
() => !document.querySelector(".raus__titel")
|
||
&& !!document.getElementById("leinwand")
|
||
&& !!document.body.dataset.stand
|
||
/* UND DIE WERBEFLAECHE IST EINGEHAENGT. Hier standen
|
||
vorher 600 Millisekunden „zur Sicherheit". Das ist
|
||
keine Bedingung, sondern eine Annahme -- und dieselbe
|
||
Sorte Annahme hat in diesem Block zweimal einen Fehler
|
||
gemeldet, den es nicht gab. Die Flaeche entsteht in
|
||
`werbungZeichnen()`, also nachdem der Stand angekommen
|
||
und verarbeitet ist. */
|
||
&& !!document.querySelector(".werbung"),
|
||
null, { timeout: 15000 });
|
||
|
||
const setz = (felder) => host.seite.evaluate(async (f) => {
|
||
const a = await fetch("/workspace/api/reaktion/werbung", {
|
||
method: "POST", credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(f),
|
||
});
|
||
return { code: a.status, text: (await a.text()).slice(0, 120) };
|
||
}, felder);
|
||
|
||
/* AUF DAS ERGEBNIS WARTEN, NICHT AUF DIE UHR. Feste Wartezeiten
|
||
stimmen meistens -- und melden, wenn daneben etwas langsamer
|
||
wird, einen Fehler, den es nicht gibt. Genau das ist hier
|
||
zweimal passiert. Laeuft die Frist ab, ist es ein echter
|
||
Befund. */
|
||
const warte = async (seite, fn, was, frist = 9000) => {
|
||
try { await seite.waitForFunction(fn, null, { timeout: frist }); return true; }
|
||
catch {
|
||
console.log(` ACHTUNG: ${was} ist nach ${frist / 1000} s nicht angekommen.`);
|
||
ausgang = 1;
|
||
return false;
|
||
}
|
||
};
|
||
|
||
/* ---- 1. Das Laufband ---- */
|
||
const an = await setz({ art: "laufband", tempo: 12 });
|
||
console.log(` Band einschalten: ${an.code}`);
|
||
await warte(zuschauer.seite,
|
||
() => document.querySelector('.werbung[data-art="laufband"]')
|
||
&& document.querySelectorAll(".werbung__stueck").length > 0,
|
||
"das Laufband");
|
||
|
||
const band = await zuschauer.seite.evaluate(() => {
|
||
const w = document.querySelector(".werbung");
|
||
const b = document.querySelector(".werbung__band");
|
||
if (!w || !b) return null;
|
||
const r = b.getBoundingClientRect();
|
||
const lein = document.getElementById("leinwand")?.getBoundingClientRect();
|
||
const stuecke = [...document.querySelectorAll(".werbung__stueck")];
|
||
const bilder = [...document.querySelectorAll(".werbung__stueck img")];
|
||
return {
|
||
art: w.dataset.art,
|
||
sichtbar: !b.hidden && r.width > 10 && r.height > 10,
|
||
hoch: Math.round(r.height),
|
||
/* STEHT ES IM BILD? Ein Band, das rechnerisch existiert und
|
||
unter dem Rand der Leinwand liegt, ist kein Band. */
|
||
imBild: !!lein && r.bottom <= lein.bottom + 2 && r.top >= lein.top - 2,
|
||
stuecke: stuecke.length,
|
||
/* `naturalWidth` und nicht „gibt es das Element": Ein Bild,
|
||
das 404 antwortet, steht genauso im Dokument wie eines,
|
||
das laedt -- auf dem Bildschirm ist dort nichts. */
|
||
bilderGeladen: bilder.filter((i) => i.naturalWidth > 0).length,
|
||
bilderGesamt: bilder.length,
|
||
/* ZEILE UND UNTERZEILE GETRENNT ABLESEN. `textContent` des
|
||
ganzen Stuecks klebt beides aneinander
|
||
(„dogfather-universe.comAlles ueber das Rudel") -- im Bild
|
||
stehen sie untereinander, im Protokoll sah es nach einem
|
||
fehlenden Leerzeichen aus. Eine Messung, die einen Fehler
|
||
ERFINDET, kostet genauso viel Zeit wie eine, die einen
|
||
verschweigt. */
|
||
texte: stuecke.slice(0, 2).map((z) => [
|
||
z.querySelector(".werbung__text")?.textContent.trim() || "",
|
||
z.querySelector(".werbung__unter")?.textContent.trim() || "",
|
||
].filter(Boolean).join(" / ")),
|
||
code: document.querySelector(".werbung__code")?.textContent || "",
|
||
};
|
||
});
|
||
if (!band) {
|
||
console.log(" ACHTUNG: beim Zuschauer gibt es gar keine Einblendung.");
|
||
ausgang = 1;
|
||
} else {
|
||
console.log(` Band: ${band.art}, ${band.hoch} px hoch, ${band.stuecke} Stuecke, `
|
||
+ `Bilder ${band.bilderGeladen}/${band.bilderGesamt}`);
|
||
console.log(` Texte: ${band.texte.join(" | ")}`);
|
||
console.log(` Rabattcode hervorgehoben: „${band.code}"`);
|
||
if (!band.sichtbar) { console.log(" ACHTUNG: das Band ist nicht zu sehen."); ausgang = 1; }
|
||
if (!band.imBild) { console.log(" ACHTUNG: das Band liegt ausserhalb der Leinwand."); ausgang = 1; }
|
||
if (band.bilderGesamt && band.bilderGeladen < band.bilderGesamt) {
|
||
console.log(" ACHTUNG: ein Bild im Band laedt nicht.");
|
||
ausgang = 1;
|
||
}
|
||
/* DAS LAUFBAND TRAEGT ZWEIMAL DIESELBE FOLGE. Ohne die zweite
|
||
entstuende am Ende jedes Durchlaufs eine leere Flaeche -- und
|
||
das sieht aus, als sei die Einblendung abgestuerzt. */
|
||
if (band.stuecke !== 4) {
|
||
console.log(` ACHTUNG: zwei Botschaften, zweimal -- erwartet 4 Stuecke, gezaehlt ${band.stuecke}.`);
|
||
ausgang = 1;
|
||
}
|
||
if (!band.code) {
|
||
console.log(" ACHTUNG: DOGI10 wurde nicht als Code erkannt.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* ---- 2. Die Ecken ---- */
|
||
const ecken = await setz({ ecke_dogi: "ol", ecke_hase: "ur", ecke_gross: 1.4 });
|
||
console.log(` Ecken setzen: ${ecken.code}`);
|
||
await warte(zuschauer.seite,
|
||
() => document.querySelector('.werbung__ecke--dogi[data-platz="ol"]:not([hidden])')
|
||
&& document.querySelector('.werbung__ecke--hase[data-platz="ur"]:not([hidden])'),
|
||
"die zwei Eckenbilder");
|
||
|
||
const lage = await zuschauer.seite.evaluate(() => {
|
||
const lein = document.getElementById("leinwand")?.getBoundingClientRect();
|
||
const lies = (welches) => {
|
||
const e = document.querySelector(`.werbung__ecke--${welches}`);
|
||
if (!e || e.hidden) return null;
|
||
const r = e.getBoundingClientRect();
|
||
const bild = e.querySelector("img");
|
||
return {
|
||
platz: e.dataset.platz,
|
||
breit: Math.round(r.width),
|
||
geladen: !!bild && bild.naturalWidth > 0,
|
||
/* GEMESSEN WIRD DER ABSTAND ZU DEN VIER KANTEN, nicht der
|
||
Name der Ecke. Eine Pruefung, die nur ihren eigenen
|
||
Namen bestaetigt, bestaetigt nichts. */
|
||
l: Math.round(r.left - lein.left), r: Math.round(lein.right - r.right),
|
||
o: Math.round(r.top - lein.top), u: Math.round(lein.bottom - r.bottom),
|
||
};
|
||
};
|
||
return { dogi: lies("dogi"), hase: lies("hase") };
|
||
});
|
||
for (const [welches, soll] of [["dogi", "ol"], ["hase", "ur"]]) {
|
||
const e = lage[welches];
|
||
if (!e) { console.log(` ACHTUNG: ${welches} steht nicht im Bild.`); ausgang = 1; continue; }
|
||
console.log(` ${welches}: ${e.platz}, ${e.breit} px, `
|
||
+ `Abstaende l/r/o/u ${e.l}/${e.r}/${e.o}/${e.u}, Bild ${e.geladen ? "laedt" : "FEHLT"}`);
|
||
if (!e.geladen) { console.log(` ACHTUNG: das Bild von ${welches} laedt nicht.`); ausgang = 1; }
|
||
const linksOben = e.l < e.r && e.o < e.u;
|
||
const rechtsUnten = e.r < e.l && e.u < e.o;
|
||
if (soll === "ol" && !linksOben) {
|
||
console.log(" ACHTUNG: dogi sollte oben links stehen."); ausgang = 1;
|
||
}
|
||
if (soll === "ur" && !rechtsUnten) {
|
||
console.log(" ACHTUNG: hase sollte unten rechts stehen."); ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* ---- 3. Zwei Bilder auf denselben Platz ---- */
|
||
const doppelt = await setz({ ecke_hase: "ol" });
|
||
console.log(` Beide auf denselben Platz: ${doppelt.code} `
|
||
+ `(400 erwartet -- ein Bild auf dem anderen ist keine Absicht)`);
|
||
if (doppelt.code !== 400) {
|
||
console.log(" ACHTUNG: zwei Bilder duerfen nicht auf demselben Platz landen.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ---- 4. Der Wechsel ---- */
|
||
await setz({ art: "wechsel", tempo: 8 });
|
||
await warte(zuschauer.seite,
|
||
() => document.querySelector('.werbung[data-art="wechsel"]')
|
||
&& document.querySelectorAll(".werbung__stueck--wechsel").length > 1,
|
||
"der Wechsel");
|
||
/* SPUR: Was passiert in der Sekunde nach dem Umschalten? Ein
|
||
einzelner Blick sagt nicht, ob etwas nie da war oder gleich
|
||
wieder weg -- und das sind zwei ganz verschiedene Ursachen. */
|
||
{
|
||
const folge = [];
|
||
for (let i = 0; i < 6; i++) {
|
||
folge.push(await zuschauer.seite.evaluate(() => {
|
||
const w = document.querySelector(".werbung");
|
||
return `${w?.dataset.art || "?"}/`
|
||
+ `${document.querySelectorAll(".werbung__stueck--wechsel").length}`;
|
||
}));
|
||
await zuschauer.seite.waitForTimeout(250);
|
||
}
|
||
console.log(` [Spur] Art/Stuecke: ${folge.join(" -> ")}`);
|
||
}
|
||
const wechsel = await zuschauer.seite.evaluate(() => {
|
||
const w = document.querySelector(".werbung");
|
||
const alle = [...document.querySelectorAll(".werbung__stueck--wechsel")];
|
||
const sichtbar = alle.filter((z) => z.dataset.dran === "ja");
|
||
return {
|
||
art: w?.dataset.art,
|
||
wieviele: alle.length,
|
||
dran: sichtbar.length,
|
||
/* UEBEREINANDER, NICHT NACHEINANDER -- gefragt ist, ob die
|
||
Hoehe des Bandes springt, wenn eine laengere Botschaft
|
||
kommt. Deshalb wird die HOEHE gemessen und keine
|
||
Koordinate: Liegen die Botschaften in derselben
|
||
Gitterzelle, ist ihr Traeger so hoch wie die hoechste von
|
||
ihnen; liegen sie untereinander, so hoch wie ihre Summe.
|
||
|
||
Zwei frueherre Fassungen haben hier Oberkanten und Mitten
|
||
verglichen und beide Male die ANIMATION abgelesen: Die
|
||
ruhende Botschaft steht um genau die sechs Pixel tiefer,
|
||
mit denen sie hereingleitet. Eine Hoehe kennt kein
|
||
`translateY`. */
|
||
traeger: (() => {
|
||
const l = document.querySelector(".werbung__lauf");
|
||
return l ? Math.round(l.getBoundingClientRect().height) : 0;
|
||
})(),
|
||
hoehen: alle.map((z) => Math.round(z.getBoundingClientRect().height)),
|
||
};
|
||
});
|
||
{
|
||
const h = wechsel.hoehen;
|
||
const summe = h.reduce((a, b) => a + b, 0);
|
||
const hoechste = Math.max(0, ...h);
|
||
/* `summe > hoechste + 10` ist die Gegenprobe: Gaebe es nur eine
|
||
Botschaft, waere „der Traeger ist so hoch wie die hoechste"
|
||
zwangslaeufig erfuellt -- gruen, ohne etwas ueber das
|
||
Uebereinanderliegen zu wissen. */
|
||
wechsel.summe = summe;
|
||
wechsel.hoechste = hoechste;
|
||
wechsel.gleicheStelle = summe > hoechste + 10
|
||
&& wechsel.traeger <= hoechste + 2;
|
||
}
|
||
console.log(` Wechsel: ${wechsel.wieviele} Botschaften, ${wechsel.dran} sichtbar, `
|
||
+ `Traeger ${wechsel.traeger} px bei Hoehen ${wechsel.hoehen.join("+")} `
|
||
+ `(uebereinander ${wechsel.hoechste}, untereinander ${wechsel.summe}), `
|
||
+ `uebereinander: ${wechsel.gleicheStelle}`);
|
||
if (wechsel.art !== "wechsel" || wechsel.dran !== 1) {
|
||
console.log(" ACHTUNG: beim Wechsel muss genau eine Botschaft dastehen.");
|
||
ausgang = 1;
|
||
}
|
||
if (!wechsel.gleicheStelle) {
|
||
console.log(" ACHTUNG: die Botschaften liegen nicht uebereinander -- die Hoehe springt.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ---- 5. Ein Bild davon ---- */
|
||
await setz({ art: "laufband", ecke_hase: "ur" });
|
||
await warte(zuschauer.seite,
|
||
() => document.querySelector('.werbung[data-art="laufband"]'),
|
||
"das Laufband fuer das Bildschirmfoto");
|
||
await bild(zuschauer.seite, "24-werbung");
|
||
|
||
/* ---- 5b. Zeigt die Eckenwahl, wo die Kameras stehen? ----
|
||
Offener Punkt vom 29.09.: „Unten rechts" liegt im Layout „Kino"
|
||
genau dort, wo die Kamerakacheln sitzen. Die Regie sagt es jetzt
|
||
vorher -- gezeigt, nicht verboten.
|
||
|
||
DIE GEGENPROBE IST DER HALBE SINN. Eine Markierung, die immer da
|
||
ist, ist keine. Sie muss auch verschwinden koennen, und zwar
|
||
genau dann, wenn es nichts zu bedenken gibt. */
|
||
{
|
||
const lies = () => host.seite.evaluate(() => {
|
||
const g = document.querySelector("#eckenwahl-dogi .eckenwahl__gitter");
|
||
return [...(g?.children || [])]
|
||
.filter((k) => k.dataset.kameras === "ja")
|
||
.map((k) => `${k.dataset.platz}:${k.title.includes("Kameras") ? "benannt" : "STUMM"}`);
|
||
});
|
||
|
||
const stelle = (koerper) => host.seite.evaluate(async (b) => {
|
||
const a = await fetch("/workspace/api/reaktion/layout", {
|
||
method: "POST", credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(b),
|
||
});
|
||
return a.status;
|
||
}, koerper);
|
||
|
||
await stelle({ layout: "kino", kamera_ecke: "ur" });
|
||
await warte(host.seite,
|
||
() => document.querySelector('#eckenwahl-dogi .eckenwahl__knopf[data-kameras="ja"]'),
|
||
"der Kamerahinweis in der Eckenwahl");
|
||
const imKino = await lies();
|
||
console.log(` Eckenwahl im Kino (Kameras unten rechts): ${imKino.join(", ") || "KEINER"}`);
|
||
if (imKino.length !== 1 || !imKino[0].startsWith("ur:")) {
|
||
console.log(" ACHTUNG: Der Hinweis steht nicht genau an der Kameraecke.");
|
||
ausgang = 1;
|
||
}
|
||
if (imKino.some((x) => x.endsWith("STUMM"))) {
|
||
console.log(" ACHTUNG: Der Hinweis ist nur eine Farbe -- wer vorlesen "
|
||
+ "laesst, bekommt ihn nicht.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* GEGENPROBE: Bei „nur Video" gibt es keine Kameras -- dann darf
|
||
auch kein Hinweis stehen. Eine Warnung vor etwas, das nicht
|
||
passieren kann, gewoehnt man sich ab. */
|
||
await stelle({ layout: "nur_video" });
|
||
await host.seite.waitForFunction(
|
||
() => document.body.dataset.stand !== undefined, null, { timeout: 5000 })
|
||
.catch(() => {});
|
||
let ohne = await lies();
|
||
for (let i = 0; i < 12 && ohne.length; i++) {
|
||
await host.seite.waitForTimeout(250);
|
||
ohne = await lies();
|
||
}
|
||
console.log(` Gegenprobe bei „nur Video": ${ohne.length} Hinweis(e)`);
|
||
if (ohne.length) {
|
||
console.log(" ACHTUNG: Der Hinweis steht auch dort, wo es gar keine "
|
||
+ "Kameras gibt.");
|
||
ausgang = 1;
|
||
}
|
||
await stelle({ layout: "kino" });
|
||
}
|
||
|
||
/* ---- 6. Wieder aus ---- */
|
||
const aus = await setz({ art: "aus", ecke_dogi: "aus", ecke_hase: "aus" });
|
||
await warte(zuschauer.seite,
|
||
() => !document.querySelector(".werbung__band:not([hidden])")
|
||
&& !document.querySelector(".werbung__ecke:not([hidden])"),
|
||
"das Abschalten");
|
||
const weg = await zuschauer.seite.evaluate(() => {
|
||
const b = document.querySelector(".werbung__band");
|
||
const e = [...document.querySelectorAll(".werbung__ecke")].filter((x) => !x.hidden);
|
||
return { band: !!b && !b.hidden, ecken: e.length };
|
||
});
|
||
console.log(` Nach dem Abschalten: Band ${weg.band ? "STEHT NOCH" : "weg"}, `
|
||
+ `${weg.ecken} Ecken uebrig`);
|
||
if (weg.band || weg.ecken) {
|
||
console.log(" ACHTUNG: abgeschaltete Werbung steht weiter im Bild.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* =====================================================================
|
||
BEENDET HEISST LEER -- auch im Kasten
|
||
=====================================================================
|
||
Filipe, 30.09.2026: „wenn eine sendung beendet wurde soll alles
|
||
automatisch verschwinden. also von daten die da stehen von dem
|
||
video."
|
||
|
||
GEMESSEN WIRD DAS EINGABEFELD, nicht die Spalte. Genau dazwischen
|
||
lag der zweite Fehler: `p-beginn` wurde nur gefuellt, nie geleert
|
||
-- die Spalte war leer, im Pult stand der Termin weiter, bis
|
||
jemand die Seite neu lud. Das betraf auch den vorhandenen Knopf
|
||
„Alles zuruecksetzen".
|
||
===================================================================== */
|
||
console.log("");
|
||
console.log("=== Beendet heisst leer (im Pult) ===");
|
||
{
|
||
const felder = () => host.seite.evaluate(() => ({
|
||
titel: document.getElementById("p-titel")?.value ?? null,
|
||
video: document.getElementById("p-video")?.value ?? null,
|
||
beginn: document.getElementById("p-beginn")?.value ?? null,
|
||
zweit: document.getElementById("p-zweit")?.value ?? null,
|
||
}));
|
||
|
||
/* Erst fuellen -- sonst misst die Pruefung leere Felder und nennt
|
||
das Erfolg. */
|
||
await host.seite.evaluate(async () => {
|
||
const wann = new Date(Date.now() + 3600000);
|
||
const p = (n) => String(n).padStart(2, "0");
|
||
await fetch("/workspace/api/reaktion", {
|
||
method: "PUT", credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
titel: "Test", video: "dQw4w9WgXcQ",
|
||
beginnt_am: wann.toISOString(),
|
||
}),
|
||
});
|
||
await fetch("/workspace/api/reaktion/stand", {
|
||
method: "POST", credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: "live" }),
|
||
});
|
||
});
|
||
await host.seite.waitForTimeout(1500);
|
||
|
||
const voll = await felder();
|
||
console.log(` Vorher im Pult: Titel="${voll.titel}" Video="${voll.video}" `
|
||
+ `Beginn="${voll.beginn}"`);
|
||
if (!voll.titel || !voll.video || !voll.beginn) {
|
||
console.log(" ACHTUNG: Die Felder liessen sich nicht fuellen -- "
|
||
+ "so ist die Frage nicht zu beantworten.");
|
||
ausgang = 1;
|
||
} else {
|
||
await host.seite.evaluate(() => fetch("/workspace/api/reaktion/stand", {
|
||
method: "POST", credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: "zu" }),
|
||
}));
|
||
await host.seite.waitForTimeout(1800);
|
||
|
||
const leer = await felder();
|
||
console.log(` Nach dem Beenden: Titel="${leer.titel}" Video="${leer.video}" `
|
||
+ `Beginn="${leer.beginn}" Zweites="${leer.zweit}"`);
|
||
for (const [name, wert] of Object.entries(leer)) {
|
||
if (wert) {
|
||
console.log(` ACHTUNG: Im Feld „${name}" steht nach dem Beenden `
|
||
+ `weiter „${wert}".`);
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Wiederherstellen -- die Messungen danach brauchen eine laufende
|
||
Sendung. */
|
||
await host.seite.evaluate(async () => {
|
||
await fetch("/workspace/api/reaktion", {
|
||
method: "PUT", credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ titel: "Reaction", video: "dQw4w9WgXcQ" }),
|
||
});
|
||
await fetch("/workspace/api/reaktion/stand", {
|
||
method: "POST", credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: "live" }),
|
||
});
|
||
});
|
||
await host.seite.waitForTimeout(1200);
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE CHATKISTE STEHT IMMER (30.09.2026)
|
||
=====================================================================
|
||
Filipe: „mach dass ich die chat kiste immer sehe bitte und nicht
|
||
erst wenn das video läuft."
|
||
|
||
Sie lag im Kino (`#teil-live`) und ging mit ihm weg. Im
|
||
Wartebereich stand dabei woertlich „Der Chat ist schon offen" --
|
||
der Satz stimmte sogar, der Server laesst dort schreiben; zu sehen
|
||
war der Chat trotzdem nicht.
|
||
|
||
GEMESSEN WIRD IN ALLEN DREI ZUSTAENDEN. Nur im Live zu messen
|
||
waere die Pruefung, die den Fehler nicht sehen kann.
|
||
===================================================================== */
|
||
console.log("");
|
||
console.log("=== Die Chatkiste in jedem Zustand ===");
|
||
{
|
||
const kiste = (seite) => seite.evaluate(() => {
|
||
const e = document.getElementById("schiene");
|
||
if (!e) return null;
|
||
const r = e.getBoundingClientRect();
|
||
const feld = document.getElementById("chat-feld");
|
||
const fr = feld ? feld.getBoundingClientRect() : null;
|
||
return {
|
||
stand: document.body.dataset.stand || "(nichts)",
|
||
breit: Math.round(r.width), hoch: Math.round(r.height),
|
||
/* SICHTBAR HEISST: hat Flaeche UND ist nicht weggeschaltet.
|
||
`display: none` gibt 0x0 -- beides zusammen laesst keinen
|
||
Zwischenweg offen. */
|
||
sichtbar: getComputedStyle(e).display !== "none" && r.width > 0 && r.height > 0,
|
||
feldBreit: fr ? Math.round(fr.width) : 0,
|
||
grund: feld ? feld.placeholder : "",
|
||
gesperrt: feld ? !!feld.disabled : null,
|
||
};
|
||
});
|
||
|
||
const stellen = async (stand, mitVideo) => {
|
||
const antwort = await host.seite.evaluate(async (w) => {
|
||
if (w.video) {
|
||
await fetch("/workspace/api/reaktion", {
|
||
method: "PUT", credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ titel: "Chatprobe", video: "dQw4w9WgXcQ" }),
|
||
});
|
||
}
|
||
const a = await fetch("/workspace/api/reaktion/stand", {
|
||
method: "POST", credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: w.stand }),
|
||
});
|
||
/* DIE ANTWORT WIRD ZURUECKGEGEBEN. Ohne sie meldet die Messung
|
||
nur „steht auf live statt auf zu" und verschweigt, dass der
|
||
Server 409 gesagt hat -- man sucht dann am falschen Ende. */
|
||
return { code: a.status, text: (await a.text()).slice(0, 80) };
|
||
}, { stand, video: mitVideo });
|
||
if (antwort.code !== 200) {
|
||
console.log(` Der Server nimmt „${stand}" nicht an: `
|
||
+ `${antwort.code} ${antwort.text}`);
|
||
}
|
||
/* 40 SEKUNDEN, UND DAS HAT EINEN GRUND. „zu" kommt sofort an --
|
||
die Empfaenger werden seit dem 30.09. vor dem Leeren der
|
||
Dabei-Liste bestimmt. Die RUECKKEHR nicht: Wer im
|
||
geschlossenen Saal sitzt, steht in keiner Liste mehr und
|
||
bekommt keinen Rundruf. Seine Seite faengt sich selbst, aber
|
||
erst mit dem eigenen Dreissig-Sekunden-Takt.
|
||
|
||
Gewartet wird auf den Zustand, nicht die volle Zeit: Kommt er
|
||
frueher, geht es frueher weiter. */
|
||
try {
|
||
await zuschauer.seite.waitForFunction(
|
||
(w) => document.body.dataset.stand === w, stand, { timeout: 40_000 });
|
||
} catch { /* unten faellt es auf */ }
|
||
await zuschauer.seite.waitForTimeout(400);
|
||
};
|
||
|
||
let fehlt = 0;
|
||
for (const [stand, wort, mitVideo] of [
|
||
["zu", "der Saal ist zu", false],
|
||
["vorbereitung", "im Wartebereich", true],
|
||
["live", "waehrend der Sendung", true],
|
||
]) {
|
||
await stellen(stand, mitVideo);
|
||
/* SPUR: Was sagt der Server, und was zeigt ihre Seite? Ohne
|
||
beides nebeneinander sucht man am falschen Ende. */
|
||
const spur = await zuschauer.seite.evaluate(async () => {
|
||
const a = await fetch("/workspace/api/reaktion", { credentials: "same-origin" });
|
||
const j = await a.json().catch(() => null);
|
||
return { code: a.status, server: j?.stand, seite: document.body.dataset.stand,
|
||
raus: document.body.dataset.raus || "nein" };
|
||
});
|
||
console.log(` [Spur] Server sagt „${spur.server}" (${spur.code}), `
|
||
+ `Seite zeigt „${spur.seite}", raus=${spur.raus}`);
|
||
const k = await kiste(zuschauer.seite);
|
||
if (!k) {
|
||
console.log(` ACHTUNG: ${wort}: die Chatkiste gibt es gar nicht.`);
|
||
ausgang = 1; fehlt++;
|
||
continue;
|
||
}
|
||
console.log(` ${wort.padEnd(22)} Stand=${k.stand.padEnd(13)} `
|
||
+ `Kiste ${k.breit}x${k.hoch} px, Feld ${k.feldBreit} px`
|
||
+ `${k.gesperrt ? ` (gesperrt: „${k.grund}")` : ""}`);
|
||
if (k.stand !== stand) {
|
||
console.log(` ACHTUNG: ${wort}: die Seite steht auf „${k.stand}" `
|
||
+ `statt auf „${stand}" -- so ist die Frage nicht zu beantworten.`);
|
||
ausgang = 1; fehlt++;
|
||
continue;
|
||
}
|
||
/* 200 px und nicht „groesser als null": Eine Spalte, die auf
|
||
zwanzig Pixel zusammenfaellt, ist rechnerisch da und auf dem
|
||
Bildschirm weg. */
|
||
if (!k.sichtbar || k.breit < 200 || k.hoch < 100 || k.feldBreit < 100) {
|
||
console.log(` ACHTUNG: ${wort}: die Chatkiste ist nicht zu sehen `
|
||
+ `(${k.breit}x${k.hoch}, Feld ${k.feldBreit}).`);
|
||
ausgang = 1; fehlt++;
|
||
}
|
||
/* WER NICHT SCHREIBEN DARF, LIEST DEN GRUND. Ein graues Feld
|
||
ohne Erklaerung laesst einen an sich selbst zweifeln. */
|
||
if (k.gesperrt && !k.grund) {
|
||
console.log(` ACHTUNG: ${wort}: das Feld ist gesperrt und sagt nicht, warum.`);
|
||
ausgang = 1; fehlt++;
|
||
}
|
||
}
|
||
if (!fehlt) console.log(" In allen drei Zustaenden steht die Kiste.");
|
||
offenePunkte.push("Wird der Saal wieder aufgemacht, erfahren die, die im "
|
||
+ "geschlossenen Saal sitzen, es erst mit ihrem eigenen 30-Sekunden-Takt "
|
||
+ "-- sie stehen in keiner Dabei-Liste mehr, an die ein Rundruf gehen "
|
||
+ "koennte. „Zu\" kommt seit dem 30.09. sofort an.");
|
||
|
||
/* Und wieder auf Sendung -- was danach kommt, braucht sie. */
|
||
await stellen("live", true);
|
||
}
|
||
|
||
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,
|
||
/* WIE VIELE ES GIBT, ZAEHLT DIE SEITE SELBST. Hier stand
|
||
frueher die feste Zahl 7; als ein achtes Register dazukam,
|
||
meldete die Pruefung „nur 8 von 7". Eine Zahl, die jemand
|
||
abschreibt, veraltet in dem Moment, in dem jemand anders
|
||
weiterbaut. */
|
||
wieviele: k.length,
|
||
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 ${l.wieviele} 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. */
|
||
/* `wieviele >= 7` ist keine zweite feste Zahl, sondern die
|
||
Untergrenze gegen den leeren Fall: Faende die Messung gar
|
||
keine Register, waere `sichtbar === wieviele` mit 0 === 0
|
||
erfuellt und die Pruefung gruen, ohne etwas angesehen zu
|
||
haben. */
|
||
if (l.rollt || l.wieviele < 7 || l.sichtbar !== l.wieviele
|
||
|| l.ueber > 0 || l.schild) {
|
||
console.log(` ACHTUNG: Die Regieleiste am Handy rollt (${l.rollt}), `
|
||
+ `zeigt nur ${l.sichtbar} von ${l.wieviele} 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);
|