Filipes Wunsch: „auch bei den videos sachen wie pause geschwindigkeit. meine kamera größer machen video kleiner. nur mein bild, nur das video, möglichkeit zwischen allem zu wechseln so wie ich will, auch gerne so dass ich vielleicht noch ein zweites nebenbei vorbereiten kann so dass ich hin und her switchen kann." GESCHWINDIGKEIT Sechs Stufen von 0,5× bis 2× (0,25× fehlt mit Absicht — bei einem Viertel klingt Sprache wie ein defektes Band). Sie gilt für ALLE: Wer sie nur bei sich umstellte, redete über eine Stelle, die die anderen noch nicht gesehen haben. Die OBS-Bühne zieht sie mit nach — ohne das liefe der Stream nach fünf Minuten auf 1,5× zweieinhalb Minuten hinter dem Saal her. Und der Zuschauer SIEHT sie: ein kleines Schild „1,5× Geschwindigkeit" auf der Leinwand, nur wenn es nicht 1× ist. Ohne das hält jeder Zweite seine Leitung für kaputt. Zwei Fallen, die dabei zugemacht wurden: Der Fünf-Sekunden-Takt des Hosts schreibt das Tempo NICHT mit (sonst drehte ein Takt mit altem Wert die Einstellung zurück), und nach jedem Videowechsel wird es neu gesetzt (YouTube stellt beim Laden auf 1 zurück). Gesetzt wird nur, was `getAvailablePlaybackRates()` hergibt — einen unmöglichen Wert ignoriert der Player schweigend, und dann stünde der Knopf auf 1,5 und es liefe 1,0. ZWEI ANORDNUNGEN MEHR „Nur Video" und „Nur Kamera" sind kein weiteres Größenverhältnis, sondern ein Weglassen — und genau das braucht OBS: Dort liegt die Kamera ohnehin als eigene Quelle, die Bühnenseite soll sie nicht doppelt zeigen. `display: none` und nicht `opacity: 0`: ein unsichtbarer YouTube-Rahmen spielt weiter und hält den Ton. Tasten 1–5, wie vorher 1–3. DAS ZWEITE VIDEO — und warum es nicht die Warteschlange ist Die Schlange ist eine Reihenfolge: eins nach dem anderen, das Gespielte ist weg. Filipe will etwas anderes — zwei Videos NEBENEINANDER, hin und her, und jedes merkt sich seine Stelle. Mit der Schlange nachgebaut wäre es eine Schlange, aus der man nie wieder herauskommt. Der Umschalter steht nur da, wenn es etwas zum Umschalten GIBT; ein Knopf, der „kein zweites Video" antwortet, ist im Live eine Falle. Getauscht wird in EINEM Schreibvorgang (BEGIN IMMEDIATE) — ein Abbruch dazwischen hätte dasselbe Video auf beiden Seiten und die gemerkte Stelle des anderen verloren. Und die Stelle kommt vom Host und nicht aus der Datenbank: dort steht der Stand vom letzten Takt, bis zu fünf Sekunden alt. Was daneben bereitliegt, sieht nur, wer sendet — es ist die Rückhand, und die verrät man nicht vorher (`zweit` ist aus `oeffentlich()` ausgenommen). DER FEHLER, DEN DIE MESSUNG GEFUNDEN HAT Bei „Nur Kamera" war der Kamerabehälter 2175 px breit in einer 1072 px breiten Leinwand — von zwei Gästen stand nur einer im Bild, der zweite lag hinter `overflow: hidden`. Auf dem Bildschirmfoto sah das aus wie eine Anordnung für einen, und niemand hätte gefragt, wo der zweite geblieben ist. Zwei Ursachen: Die Leinwand ist ein Raster mit einer Spalte nach Inhaltsbreite — ein zu breites Kind im Fluss zieht die Spalte mit, und `width: 100%` bezog sich danach auf die gewachsene Spalte. Die Begrenzung wuchs also mit dem, was sie begrenzen sollte; `max-width: calc(50% - 8px)` rechnete gegen denselben Wert und war wirkungslos. Jetzt `position: absolute; inset: 0` (kann das Raster nicht mehr aufziehen) und `flex: 1 1 0; min-width: 0` an den Fenstern — eine Regel, die nicht rechnet, kann sich nicht verrechnen. Die Messung prüft seitdem bei JEDER Anordnung, ob alle Kamerafenster innerhalb der Leinwand liegen. GEMESSEN, NICHT ANGENOMMEN Dass in der Datenbank 1.5 steht, sagt nichts darüber, ob das Video schneller läuft. Die Messung liest deshalb die Uhr der Regie zweimal ab und rechnet nach: 1× → 1,00 Sekunden je Sekunde, 2× → 2,00. Der Umschalter ebenso: hin, zurück, und die Uhr steht wieder bei 79 s statt bei 0. GEPRUEFT pruef-reaktion: 260 Prüfungen (vorher 216), 0 Fehler. pruef-buehne 36, pruef-struktur, pruef-meldungen, pruef-tippziele, pruef-css-klassen alle grün. mess-reaktion: Rückgabewert 0, kein einziges ACHTUNG. Co-Authored-By: Claude Opus 5 <[email protected]>
1200 lines
52 KiB
JavaScript
1200 lines
52 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";
|
|
|
|
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");
|
|
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
|
"-keyout", schluesselDatei, "-out", zertDatei, "-days", "2",
|
|
"-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`], { stdio: "ignore" });
|
|
|
|
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 },
|
|
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;
|
|
/* LAEUFT DAS VIDEO? Ein Zuschauer hat keine Bedienung -- startet es
|
|
nicht von selbst, sitzt er eine Stunde vor einem Standbild und
|
|
kann nichts dagegen tun. Gemessen war genau das der Fall. */
|
|
if (videoLaeuft < 2) {
|
|
console.log(` ACHTUNG: Das Video laeuft nur bei ${videoLaeuft} von 2.`);
|
|
ausgang = 1;
|
|
}
|
|
const beimHostAn = Object.values(beimHost).filter((m) => m.breite > 0).length;
|
|
if (beimHostAn < Object.keys(beimHost).length) {
|
|
console.log(" ACHTUNG: Der Host sieht nicht alle Kameras.");
|
|
ausgang = 1;
|
|
}
|
|
if (!zaehlt) {
|
|
console.log(" ACHTUNG: Es kommt kein einziges Bild an.");
|
|
ausgang = 1;
|
|
}
|
|
const stehen = [...Object.values(beiZuschauer), ...Object.values(beimHost)]
|
|
.filter((m) => m.breite > 0 && !m.laeuft).length;
|
|
if (stehen) {
|
|
console.log(` ACHTUNG: ${stehen} Bild(er) kommen an, stehen aber still `
|
|
+ "-- ein Video mit Ton startet nicht von allein.");
|
|
ausgang = 1;
|
|
}
|
|
|
|
/* ---- Die Regie ---------------------------------------------------
|
|
NICHT UMSCHALTEN, SONDERN HERSTELLEN. Ein Klick auf „Griff" heisst
|
|
„das Gegenteil von dem, was gerade ist" -- und setzt damit voraus,
|
|
dass die Messung den Zustand vorher kennt. Genau daran ist sie
|
|
einmal gescheitert: Seit das Pult aufgeklappt startet, hat der
|
|
Klick es ZUgemacht, und auf dem Bild war von der Regie nichts zu
|
|
sehen. */
|
|
console.log("");
|
|
console.log(" --- Die Regie ---");
|
|
await host.seite.evaluate(() => {
|
|
const p = document.getElementById("pult");
|
|
if (p && p.dataset.auf !== "ja") document.getElementById("pult-griff")?.click();
|
|
});
|
|
await host.seite.waitForTimeout(900);
|
|
|
|
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 .stufe[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 .stufe[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;
|
|
}
|
|
|
|
/* 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({
|
|
betrag: n.querySelector(".spendenkarte__betrag")?.textContent?.trim(),
|
|
wer: n.querySelector(".spendenkarte__wer")?.textContent?.trim(),
|
|
stufe: n.querySelector(".spendenkarte__stufe")?.textContent?.trim(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}).observe(t, { childList: true });
|
|
});
|
|
|
|
await roh("/workspace/api/spenden", {
|
|
method: "POST", keks: k.admin,
|
|
rumpf: { betrag_cent: 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. Eine Meldung ohne Bestaetigung darf NICHT laufen. */
|
|
await roh("/workspace/api/spenden/melden", {
|
|
method: "POST", keks: k.gast, rumpf: { betrag_cent: 50000 },
|
|
});
|
|
await zuschauer.seite.waitForTimeout(1200);
|
|
|
|
const nachMelden = await zuschauer.seite.evaluate(() => window.__karten);
|
|
console.log(` Karten bei der Zuschauerin: ${JSON.stringify(nachMelden)}`);
|
|
if (nachMelden.length !== 1 || !/25/.test(nachMelden[0]?.betrag || "")) {
|
|
console.log(" ACHTUNG: Die eingetragene Spende kommt nicht als Karte an.");
|
|
ausgang = 1;
|
|
}
|
|
if (nachMelden.some((k2) => /500/.test(k2.betrag || ""))) {
|
|
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.betrag_cent / 100 + " EUR von " + wartet.name : "(nichts)"}`);
|
|
if (!wartet) {
|
|
console.log(" ACHTUNG: Die Meldung kommt nicht im Pult an.");
|
|
ausgang = 1;
|
|
} else {
|
|
await roh(`/workspace/api/spenden/${wartet.id}/bestaetigen`, {
|
|
method: "POST", keks: k.admin, rumpf: { betrag_cent: 1000 },
|
|
});
|
|
/* 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 || !/10/.test(nachJa[1]?.betrag || "")) {
|
|
console.log(" ACHTUNG: Die bestaetigte Spende laeuft nicht -- oder mit falschem Betrag.");
|
|
ausgang = 1;
|
|
}
|
|
/* DIE STUFE MUSS ZUM BETRAG PASSEN. 25 EUR ist die hoechste
|
|
(„Rudel-Legende"), 10 EUR 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}`);
|
|
/* 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 zum Betrag.");
|
|
ausgang = 1;
|
|
}
|
|
}
|
|
|
|
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. */
|
|
/* ---- 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;
|
|
}
|
|
|
|
await host.seite.fill("#p-zweit", "https://youtu.be/9bZkp7q19f0").catch(() => {});
|
|
await host.seite.click("#p-zweit-sichern").catch(() => {});
|
|
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. */
|
|
await modi.seite.click("#chat-liste .beitrag:last-child .beitrag__name").catch(() => {});
|
|
await modi.seite.waitForTimeout(400);
|
|
const menue = await modi.seite.evaluate(() => {
|
|
const 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) {
|
|
console.log(" ACHTUNG: Das Moderationsmenue geht nicht auf.");
|
|
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"). */
|
|
await modi.seite.click('.modmenue__knopf:text("10 Minuten stumm")').catch(async () => {
|
|
await modi.seite.evaluate(() => {
|
|
const k2 = [...document.querySelectorAll(".modmenue__knopf")]
|
|
.find((x) => x.textContent.includes("stumm"));
|
|
if (k2) k2.click();
|
|
});
|
|
});
|
|
await modi.seite.waitForTimeout(1500);
|
|
|
|
const beimGast = await zuschauer.seite.evaluate(() => {
|
|
const f = document.getElementById("chat-feld");
|
|
return { gesperrt: !!f?.disabled, grund: f?.placeholder || "" };
|
|
});
|
|
console.log(` Die Stummgeschaltete sieht: gesperrt=${beimGast.gesperrt}, `
|
|
+ `Grund „${beimGast.grund}"`);
|
|
const serverSagt = await roh("/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast, rumpf: { text: "und jetzt?" },
|
|
});
|
|
console.log(` ... und der Server: ${serverSagt.code}/${serverSagt.j?.fehler}`);
|
|
if (!beimGast.gesperrt || !beimGast.grund.includes("stumm")
|
|
|| serverSagt.j?.fehler !== "stumm_geschaltet") {
|
|
console.log(" ACHTUNG: Die Stummschaltung kommt nicht oder mit falschem Grund an.");
|
|
ausgang = 1;
|
|
}
|
|
await roh(`/workspace/api/reaktion/massnahme/${IDS.gast}`, { method: "DELETE", keks: k.admin });
|
|
await modi.seite.waitForTimeout(900);
|
|
|
|
/* MELDEN -- der Weg fuer die, die selbst nichts tun koennen. */
|
|
const letzter = await roh("/workspace/api/reaktion/chat", { keks: k.admin });
|
|
const zuMelden = (letzter.j?.beitraege || []).filter((b) => b.person_id === IDS.gast2).pop();
|
|
if (zuMelden) {
|
|
await roh(`/workspace/api/reaktion/chat/${zuMelden.id}/melden`, {
|
|
method: "POST", keks: k.gast, rumpf: { grund: "unfreundlich" },
|
|
});
|
|
}
|
|
await modi.seite.waitForTimeout(1800);
|
|
const chip = await modi.seite.evaluate(() => {
|
|
const k2 = document.getElementById("chat-meldungen");
|
|
if (!k2) return null;
|
|
const r = k2.getBoundingClientRect();
|
|
return { versteckt: !!k2.hidden, wort: k2.textContent.trim(), hoch: Math.round(r.height) };
|
|
});
|
|
console.log(` Meldungs-Zeichen: „${chip?.wort}" (versteckt=${chip?.versteckt}, `
|
|
+ `${chip?.hoch} px hoch)`);
|
|
if (!chip || chip.versteckt || !/Meldung/.test(chip.wort)) {
|
|
console.log(" ACHTUNG: Die Meldung erreicht die Moderation nicht.");
|
|
ausgang = 1;
|
|
}
|
|
await modi.seite.click("#chat-meldungen").catch(() => {});
|
|
await modi.seite.waitForTimeout(400);
|
|
const liste = await modi.seite.evaluate(() => {
|
|
const m = document.querySelector(".modmenue--liste");
|
|
if (!m) return null;
|
|
const r = m.getBoundingClientRect();
|
|
const knoepfe = [...m.querySelectorAll(".modmenue__knopf")];
|
|
const greifbar = knoepfe.filter((k2) => {
|
|
const q = k2.getBoundingClientRect();
|
|
const oben = document.elementFromPoint(q.left + q.width / 2, q.top + q.height / 2);
|
|
return !!oben && (k2 === oben || k2.contains(oben));
|
|
}).length;
|
|
return {
|
|
zeilen: m.querySelectorAll(".modmeldung").length,
|
|
text: m.querySelector(".modmeldung__text")?.textContent || "",
|
|
knoepfe: knoepfe.length, greifbar,
|
|
rechts: Math.round(r.right), unten: Math.round(r.bottom),
|
|
fensterBreit: window.innerWidth, fensterHoch: window.innerHeight,
|
|
};
|
|
});
|
|
if (!liste || liste.zeilen < 1) {
|
|
console.log(" ACHTUNG: Die Meldungsliste geht nicht auf.");
|
|
ausgang = 1;
|
|
} else {
|
|
console.log(` ${liste.zeilen} Meldung(en), erste: „${liste.text}" -- `
|
|
+ `${liste.greifbar}/${liste.knoepfe} Knoepfe anfassbar`);
|
|
if (liste.rechts > liste.fensterBreit || liste.unten > liste.fensterHoch) {
|
|
console.log(" ACHTUNG: Die Meldungsliste steht aus dem Bild heraus.");
|
|
ausgang = 1;
|
|
}
|
|
if (liste.greifbar !== liste.knoepfe) {
|
|
console.log(" ACHTUNG: Ein Knopf der Meldungsliste ist verdeckt.");
|
|
ausgang = 1;
|
|
}
|
|
}
|
|
await bild(modi.seite, "13-meldungen");
|
|
if (modi.konsole.length) console.log(" KONSOLE:", modi.konsole.slice(0, 4).join(" | "));
|
|
|
|
/* AUS DER SENDUNG -- der haerteste Fall. Wer rausgenommen wird, muss
|
|
es ERFAHREN; vorher lief bei ihm das Video weiter, und nur die
|
|
Anwesenheitsmeldung schlug still fehl. */
|
|
await roh("/workspace/api/reaktion/massnahme", {
|
|
method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast, art: "raus" },
|
|
});
|
|
await zuschauer.seite.waitForTimeout(2500);
|
|
const rausgesetzt = await zuschauer.seite.evaluate(() => {
|
|
const k2 = document.querySelector(".raus");
|
|
return {
|
|
da: !!k2, markiert: document.body.dataset.raus === "ja",
|
|
titel: k2?.querySelector(".raus__titel")?.textContent || "",
|
|
deckt: k2 ? Math.round(k2.getBoundingClientRect().height) : 0,
|
|
fensterHoch: window.innerHeight,
|
|
};
|
|
});
|
|
console.log(` Die Rausgenommene sieht: „${rausgesetzt.titel}" `
|
|
+ `(${rausgesetzt.deckt} von ${rausgesetzt.fensterHoch} px)`);
|
|
if (!rausgesetzt.da || !rausgesetzt.markiert
|
|
|| rausgesetzt.deckt < rausgesetzt.fensterHoch - 2) {
|
|
console.log(" ACHTUNG: Wer rausgenommen wird, merkt es nicht.");
|
|
ausgang = 1;
|
|
}
|
|
await bild(zuschauer.seite, "14-aus-der-sendung");
|
|
await roh(`/workspace/api/reaktion/massnahme/${IDS.gast}`, { method: "DELETE", keks: k.admin });
|
|
|
|
await modi.kontext.close();
|
|
await host.kontext.close();
|
|
await gast2.kontext.close();
|
|
await zuschauer.kontext.close();
|
|
|
|
{
|
|
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 */ }
|
|
console.log("\nFertig.");
|
|
process.exit(ausgang);
|