Filipe, 29.09.2026: „ich will dass du die benarichtigungen
perfektionierst. dogfather und alle anderen sollen die
benachrichtigungen perfekt von dieser seite hier bekommen."
Und dazu, mit einer Bildschirmaufnahme: „wieso haengen die videos
immer."
=====================================================================
1. WARUM DIE VIDEOS HAENGEN -- zwei Zeilen, eine Rueckkopplung
=====================================================================
Die Aufnahme zeigt den YouTube-Zaehler bei 0:13 von 31:34, vier
Bilder lang unbewegt, in der Mitte der Ladering.
a) `hostMelden()` rechnete `laeuft = getPlayerState() === 1`.
Zustand 3 heisst PUFFERN -- „ich will spielen, mir fehlen
gerade Daten". Der Host meldete in diesem Moment „laeuft
nicht", und zwar sofort, weil `onStateChange` bei jedem
Zustandswechsel meldet.
b) `empfaenger()` fuegt den Host ausdruecklich hinzu, und
`folgen(d)` lief im Ereignisstrom fuer alle -- auch fuer ihn.
Seine eigene Meldung kam zurueck und traf dort auf
if (!stand.laeuft && getPlayerState() === 1) pauseVideo();
Der Host puffert eine halbe Sekunde, laeuft weiter -- und wird vom
Echo seines eigenen Pufferers angehalten. Bei einem 31-Minuten-Video
passiert das in den ersten Sekunden zuverlaessig.
Und alle Zuschauer bekamen bei JEDEM Pufferer des Hosts ein
Pause-Play-Paar. Das ist das Ruckeln, das man fuer die eigene
Leitung haelt.
GEMESSEN, NICHT HERGELEITET (mess-reaktion):
vorher Host=laeuft -> Pufferer gemeldet -> Host=pause
nachher Host=laeuft -> Pufferer gemeldet -> Host=laeuft
Drei neue Messungen, jede mit Gegenprobe:
· ein gemeldeter Pufferer haelt den Host nicht an
· ein ECHTES Anhalten (Knopf gedrueckt) haelt alle an -- sonst
waere das Erste mit kaputtem Gleichlauf bezahlt
· beim Puffern meldet der Server `laeuft=true`; ist der Pufferer
nicht herzustellen, sagt die Messung das (dritter Ausgang)
Beide Behebungen einzeln zurueckgenommen: beide Male rot.
Die erste Fassung der Gegenprobe war selbst falsch -- sie hielt das
Selbstheilen des Systems (der Host meldet alle fuenf Sekunden die
Wahrheit) fuer einen Fehler. Deshalb drueckt sie jetzt den Knopf,
statt eine Meldung zu faelschen.
=====================================================================
2. DIE REACTION LAEDT EIN
=====================================================================
Nachgemessen war `workspace-reaktion.js` STUMM: kein einziges
`benachrichtige`. Beim Einschalten lief `melden("reaktion", ...)`
ueber den Ereignisstrom -- also nur an Leute, die die Seite ohnehin
offen haben. Das Kino machte auf, und die Einladung verliess das Haus
nie. Dasselbe Muster wie bei den Bewerbungen am 23.09.
Neue Art `reaktion_live`, eigene neben `dogfather_live`: Das eine ist
sein Stream auf TikTok, das andere das Kino hier im Haus.
Eingeladen wird, wer ein Geraet hat und NICHT der Agentur gehoert
(`AGENTUR_ROLLEN` -- keine neue Liste). Nicht der, der eingeschaltet
hat. Und nicht zweimal: Eine Bremse von zwei Stunden faengt den
Neustart ab, denn das Merkmal haengt an `gestartet_am` und das wird
bei jedem Wechsel nach live neu gesetzt.
Geprueft Ende zu Ende in pruef-reaktion (+10): Sendung geht auf
Sendung, danach stehen genau fuenf Einladungen in `push_verschickt` --
rechte Hand, linke Hand, Modi, zweimal Community. Nicht der Host,
nicht die Creatorin. Einladung abgeschaltet: sechs Fehlschlaege.
=====================================================================
3. DARF DAS NACHTS KOMMEN? -- drei Antworten statt zwei
=====================================================================
Hier stand `art === "test" || art === "anruf"`, 230 Zeilen von der
Artenliste entfernt. Am 18.09. hat das eine Nacht lang alle Anrufe
verschluckt; behoben wurde damals dieser eine Fall.
GEMESSEN AM LIVE-BESTAND: `dogfather_live` ging an allen sieben
Abenden vom 22. bis 28.09. zwischen 20:28 und 21:06 raus -- jedes Mal
knapp vor der Sperre um 22 Uhr. Geht Filipe einmal um 22:05 live,
bekommt niemand etwas, und niemand erfaehrt warum.
`ruhe` steht jetzt an der ART:
"immer" (Vorgabe) 22-7 gesperrt -- Erinnerungen
"spaet" nur 1-7 -- etwas laeuft GERADE
"nie" nie -- ein Mensch wartet am Hoerer
=====================================================================
4. JEDES HAUS BEKOMMT SEIN GESICHT
=====================================================================
Im Service Worker stand fest `workspace-192.png`. Von acht Menschen
mit angemeldetem Geraet gehoeren fuenf ins Crew-Haus -- die sahen auf
jeder Benachrichtigung das Symbol des Hauses, in dem sie nicht
arbeiten.
Entschieden wird es auf dem SERVER: Im Browser stuende sonst die
verborgene Crew-Adresse in einer Datei ohne Anmeldung (der Befund vom
21.09. bei crew-haus.css) -- und es waere eine zweite Fassung der
Hausteilung. Zweistufig, beide Stufen gibt es schon: die Zielseite,
wenn sie in genau ein Haus gehoert (GEHOERT_ZU_ADRESSE), sonst
`AGENTUR_ROLLEN`.
=====================================================================
5. DAS ABZEICHEN WAR EIN KLOTZ
=====================================================================
`badge` ist das winzige Zeichen in der Statusleiste; Android benutzt
davon NUR den Alphakanal. Dort stand dasselbe `workspace-192.png` --
ein vollflaechiges Quadrat ohne Transparenz, also ein ausgefuellter
Klotz. Es stuerzt nichts ab, deshalb faellt es niemandem auf.
`assets/img/abzeichen-96.png`: der gefuellte Umriss des Huskys, weiss
auf durchsichtig, 3,5 KB. Drei Fassungen gebaut und bei ECHTER Groesse
(24 px) verglichen -- Strichbild zerfaellt, Flaeche traegt.
`server/helfer-png-alpha.mjs` liest den Alphakanal wirklich (PNG
auspacken, Zeilenfilter zuruecknehmen). Eine Pruefung auf „die Datei
gibt es" haette den Klotz nie gefunden -- die Datei gab es ja. Die
Gegenprobe ist der alte Zustand selbst: dieselbe Rechnung meldet auf
`workspace-192.png` „kein Alphakanal, Deckung 100 %".
Das Abzeichen liegt NICHT bei den App-Symbolen -- `pruef-struktur`
verlangt dort zu jedem Namen den vollen Satz und hielt es fuer eine
neue App. Der Waechter hat recht: Ein Abzeichen ist kein App-Symbol.
=====================================================================
Gemessen: mess-reaktion 0, pruef-reaktion 393/0 (+10),
pruef-push-ziel 38/0 (+27), pruef-push 24/0, pruef-push-weg 20/0,
pruef-glocke 36/0, pruef-struktur 44/0, pruef-haus-trennung 100/0.
Co-Authored-By: Claude Opus 5 <[email protected]>
2959 lines
134 KiB
JavaScript
2959 lines
134 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. */
|
||
await host.seite.evaluate(() => document.getElementById("v-spiel")?.click());
|
||
await host.seite.waitForTimeout(1800);
|
||
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 host.seite.waitForTimeout(2000);
|
||
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);
|
||
}
|
||
}
|
||
|
||
/* ---- Die Regie ---------------------------------------------------
|
||
NICHT UMSCHALTEN, SONDERN HERSTELLEN. Ein Klick auf „Griff" heisst
|
||
„das Gegenteil von dem, was gerade ist" -- und setzt damit voraus,
|
||
dass die Messung den Zustand vorher kennt. Genau daran ist sie
|
||
einmal gescheitert: Seit das Pult aufgeklappt startet, hat der
|
||
Klick es ZUgemacht, und auf dem Bild war von der Regie nichts zu
|
||
sehen. */
|
||
console.log("");
|
||
console.log(" --- Die Regie ---");
|
||
await host.seite.evaluate(() => {
|
||
const p = document.getElementById("pult");
|
||
if (p && p.dataset.auf !== "ja") document.getElementById("pult-griff")?.click();
|
||
});
|
||
await host.seite.waitForTimeout(900);
|
||
|
||
/* ---- Der Regieplatz: drei Kacheln und die Leiste unten ----------
|
||
|
||
Filipe: „kann das nicht bissl aufgeteilt sein auf links und rechts
|
||
und eine barre unten in der mitte."
|
||
|
||
GEMESSEN WIRD DIE LAGE UND NICHT DIE REGEL. Dass im Stilblatt
|
||
`grid-template-areas` steht, sagt nichts darueber, ob die rechte
|
||
Spalte auch wirklich rechts landet -- eine spaetere Regel, eine zu
|
||
breite Zelle, ein langer Titel, und sie steht darunter. Geprueft
|
||
wird deshalb an den Kanten:
|
||
linke Spalte endet, BEVOR die rechte anfaengt
|
||
die Leiste liegt UNTER beiden und ist so breit wie beide
|
||
Und zusaetzlich, dass nichts seitlich hinauslaeuft: Der Umbau hat
|
||
eine 16-rem-Spalte gegen einen Videotitel ohne Grenze gestellt. */
|
||
{
|
||
const platz = await host.seite.evaluate(() => {
|
||
const k = (w) => {
|
||
const e = document.querySelector(w);
|
||
if (!e) return null;
|
||
const r = e.getBoundingClientRect();
|
||
return {
|
||
l: Math.round(r.left), r: Math.round(r.right),
|
||
o: Math.round(r.top), u: Math.round(r.bottom),
|
||
b: Math.round(r.width), h: Math.round(r.height),
|
||
};
|
||
};
|
||
const innen = document.getElementById("pult-innen");
|
||
return {
|
||
/* DREI KACHELN, NICHT ZWEI SEITEN. In der 360 Pixel schmalen
|
||
Regiespalte stehen sie UNTEREINANDER -- das ist seit dem
|
||
Umbau „Regie links" der Sinn, nicht ein Fehler. Gefragt
|
||
ist deshalb: Gibt es alle drei, und hat jede Inhalt? */
|
||
laeuft: k(".regiekachel.kachel--laeuft"),
|
||
sendung: k(".regiekachel.kachel--sendung"),
|
||
bereit: k(".regiekachel.kachel--bereit"),
|
||
leiste: k(".transport"),
|
||
spiel: k("#v-spiel"),
|
||
/* Laeuft etwas seitlich hinaus? `scrollWidth` groesser als die
|
||
sichtbare Breite heisst genau das. */
|
||
ueber: innen ? innen.scrollWidth - innen.clientWidth : -1,
|
||
/* ==== UND IST DIE LEISTE UEBERHAUPT ZU SEHEN? ============
|
||
Beim ersten Bau lag sie bei y=986 in einem 900 Pixel hohen
|
||
Fenster -- die Rechnung ging auf, und man kam trotzdem nur
|
||
mit Scrollen an den Play-Knopf. Geprueft wird deshalb
|
||
gegen den SICHTBAREN Ausschnitt der Regie, nicht gegen die
|
||
Seite. */
|
||
sichtbar: (() => {
|
||
/* GEGEN DAS FENSTER, NICHT GEGEN DIE REGIE.
|
||
Die erste Fassung fragte, ob die Leiste innerhalb von
|
||
`#pult-innen` liegt. Seit dem 28.09. liegt sie gar nicht
|
||
mehr in der Regie, sondern als eigene Zeile UNTER dem
|
||
ganzen Saal -- die Antwort war also immer „nein", und
|
||
die Messung meldete dauerhaft einen Fehler, den es nicht
|
||
gab. Gefragt ist, was Filipe im Live angeht: Komme ich
|
||
an den Play-Knopf, ohne zu scrollen? */
|
||
const e = document.querySelector(".transport");
|
||
if (!e) return false;
|
||
const r = e.getBoundingClientRect();
|
||
return r.bottom <= window.innerHeight + 1 && r.top >= -1;
|
||
})(),
|
||
/* ==== UND SIE IST DIE LETZTE ZEILE ========================
|
||
„Unten" laesst sich bei einer KLEBENDEN Leiste nicht mehr an
|
||
den Kanten ablesen: Beim Hochscrollen schwebt sie
|
||
absichtlich ueber dem unteren Teil der Spalten. Die Frage
|
||
„liegt sie unter beiden?" war die von vorher -- sie hat nach
|
||
der Reparatur einen Fehler gemeldet, wo das Verhalten
|
||
richtig ist.
|
||
Was „unten" wirklich heisst, steht in der Reihenfolge: Sie
|
||
kommt im Dokument NACH beiden Spalten. Das gilt beim
|
||
Scrollen wie beim Stillstand. */
|
||
letzte: (() => {
|
||
const e = document.querySelector(".transport");
|
||
const r = document.querySelector(".regiekachel.kachel--sendung");
|
||
if (!e || !r) return false;
|
||
/* 4 = DOCUMENT_POSITION_FOLLOWING */
|
||
return !!(r.compareDocumentPosition(e) & 4);
|
||
})(),
|
||
klebt: (() => {
|
||
const e = document.querySelector(".transport");
|
||
return e ? getComputedStyle(e).position : "(?)";
|
||
})(),
|
||
fenster: window.innerHeight,
|
||
};
|
||
});
|
||
const p = platz;
|
||
console.log(` Regieplatz: Spalte ${p.laeuft?.l}..${p.laeuft?.r} · `
|
||
+ `Leiste ${p.leiste?.l}..${p.leiste?.r} bei y=${p.leiste?.o} (${p.leiste?.h} px hoch)`);
|
||
/* ==== VERDECKT DIE KLEBENDE LEISTE ETWAS? ==================
|
||
Sie schwebt ueber dem unteren Teil der Regie -- das ist
|
||
gewollt. Was NICHT gewollt ist: dass ein Knopf darunter
|
||
liegt und deshalb nicht mehr gedrueckt werden kann. Gemessen
|
||
wird, was der Browser selbst sagt: Liegt an der Mitte eines
|
||
Knopfes etwas anderes obenauf? */
|
||
const verdeckt = await host.seite.evaluate(() => {
|
||
const raus = [];
|
||
const leiste = document.querySelector(".transport");
|
||
if (!leiste) return raus;
|
||
for (const e of document.querySelectorAll("#pult-innen button, #pult-innen input,"
|
||
+ " #pult-innen select, #pult-innen label")) {
|
||
const r = e.getBoundingClientRect();
|
||
if (r.width === 0 || r.height === 0) continue;
|
||
const oben = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
|
||
if (!oben || e === oben || e.contains(oben)) continue;
|
||
if (!leiste.contains(oben)) continue; /* nur die Leiste zaehlt */
|
||
raus.push(`${e.id || e.className || e.tagName} (${Math.round(r.top)}..${Math.round(r.bottom)})`);
|
||
}
|
||
return raus;
|
||
});
|
||
if (verdeckt.length) {
|
||
console.log(` ACHTUNG: die Transportleiste liegt ueber ${verdeckt.length} Bedienelement(en): `
|
||
+ verdeckt.slice(0, 5).join(", "));
|
||
ausgang = 1;
|
||
} else {
|
||
console.log(" Die Transportleiste verdeckt kein Bedienelement der Regie.");
|
||
}
|
||
|
||
|
||
console.log(` Play-Knopf ${p.spiel?.b}x${p.spiel?.h} px · seitlich ueber ${p.ueber} px `
|
||
+ `· Leiste ohne Scrollen sichtbar: ${p.sichtbar} `
|
||
+ `(${p.klebt}, nach beiden Spalten: ${p.letzte})`);
|
||
if (!p.sichtbar) {
|
||
console.log(" ACHTUNG: Die Transportleiste ist nur mit Scrollen zu erreichen.");
|
||
ausgang = 1;
|
||
}
|
||
if (!p.laeuft || !p.sendung || !p.bereit || !p.leiste) {
|
||
console.log(` ACHTUNG: Es fehlt eine Kachel (laeuft=${!!p.laeuft}, `
|
||
+ `sendung=${!!p.sendung}, bereit=${!!p.bereit}, Leiste=${!!p.leiste}).`);
|
||
ausgang = 1;
|
||
} else {
|
||
/* UNTEREINANDER, NICHT NEBENEINANDER. In der schmalen
|
||
Regiespalte ist das die Absicht: Jede Kachel bekommt die
|
||
volle Breite, und nichts muss seitlich geschoben werden.
|
||
Geprueft wird deshalb, dass sie sich NICHT ueberlappen und
|
||
jede die Spalte fuellt -- zwei halbe Kacheln nebeneinander
|
||
waeren in 360 Pixeln unlesbar. */
|
||
const untereinander = p.sendung.o >= p.laeuft.u - 1
|
||
&& p.bereit.o >= p.sendung.u - 1;
|
||
console.log(` Kacheln (y): laeuft ${p.laeuft.o}..${p.laeuft.u} · sendung ${p.sendung.o}..${p.sendung.u}`
|
||
+ ` · bereit ${p.bereit.o}..${p.bereit.u} · alle ${p.laeuft.b} px breit`
|
||
+ ` · untereinander: ${untereinander}`);
|
||
if (!untereinander) {
|
||
console.log(" ACHTUNG: Die drei Kacheln stehen nicht untereinander.");
|
||
ausgang = 1;
|
||
}
|
||
/* DIE LEISTE KOMMT NACH ALLEM. `position: sticky` braucht sie
|
||
seit dem Umbau „Regie links" nicht mehr: Sie ist eine eigene
|
||
Zeile des Saals und steht damit von selbst unten. Was gilt,
|
||
ist die Reihenfolge -- und die gilt beim Scrollen wie beim
|
||
Stillstand. */
|
||
if (!p.letzte) {
|
||
console.log(` ACHTUNG: Die Leiste kommt nicht nach den Kacheln.`);
|
||
ausgang = 1;
|
||
}
|
||
/* Sie soll ueber die GANZE Breite des Saals gehen -- eine
|
||
Leiste, die nur so breit ist wie die Regiespalte, waere
|
||
keine Leiste unten, sondern ein Anhaengsel der Regie. */
|
||
if (p.leiste.b < p.laeuft.b * 2) {
|
||
console.log(` ACHTUNG: Die Leiste ist nur ${p.leiste.b} px breit `
|
||
+ `(die Regiespalte allein ist ${p.laeuft.b} px).`);
|
||
ausgang = 1;
|
||
}
|
||
/* Der Play-Knopf ist der einzige, den man im Live blind treffen
|
||
muss. 44 px ist die Hausgrenze; er soll deutlich darueber
|
||
liegen. */
|
||
if ((p.spiel?.b || 0) < 52 || (p.spiel?.h || 0) < 52) {
|
||
console.log(" ACHTUNG: Der Play-Knopf ist zu klein zum Blindtreffen.");
|
||
ausgang = 1;
|
||
}
|
||
if (p.ueber > 1) {
|
||
console.log(` ACHTUNG: Der Regieplatz laeuft ${p.ueber} px seitlich hinaus.`);
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* ---- Und der Knopf zum Teilen ---- */
|
||
const teilen = await host.seite.evaluate(() => {
|
||
const e = document.getElementById("s-teilen");
|
||
if (!e) return null;
|
||
const r = e.getBoundingClientRect();
|
||
return {
|
||
da: r.width > 0 && r.height > 0,
|
||
b: Math.round(r.width), h: Math.round(r.height),
|
||
wort: document.getElementById("s-teilen-wort")?.textContent || "",
|
||
gedrueckt: e.getAttribute("aria-pressed"),
|
||
};
|
||
});
|
||
console.log(` Bildschirm teilen: „${teilen?.wort}" ${teilen?.b}x${teilen?.h} px, `
|
||
+ `gedrueckt=${teilen?.gedrueckt}`);
|
||
if (!teilen?.da || teilen.h < 40 || teilen.wort !== "Bildschirm") {
|
||
console.log(" ACHTUNG: Der Knopf zum Teilen fehlt oder ist zu klein.");
|
||
ausgang = 1;
|
||
}
|
||
await bild(host.seite, "23-regieplatz");
|
||
}
|
||
|
||
const regie = await host.seite.evaluate(() => {
|
||
const t = (id) => document.getElementById(id)?.textContent?.trim() || "";
|
||
const sicht = (w) => {
|
||
const e = document.querySelector(w);
|
||
if (!e) return false;
|
||
const r = e.getBoundingClientRect();
|
||
return r.width > 0 && r.height > 0;
|
||
};
|
||
return {
|
||
auf: document.getElementById("pult")?.dataset.auf,
|
||
lampe: t("pult-wort"),
|
||
uhr: t("pult-zeit"),
|
||
dabei: t("z-dabei"),
|
||
sicht_zahl: t("z-sicht"),
|
||
last: t("z-last"),
|
||
spur: t("spur-titel"),
|
||
zeit: t("v-zeit"),
|
||
reiter: document.querySelectorAll("#reiter .reiter__knopf").length,
|
||
tafeln: document.querySelectorAll(".pult-tafel").length,
|
||
tafel_sichtbar: [...document.querySelectorAll(".pult-tafel")].filter((e) => !e.hidden).length,
|
||
regler: document.querySelectorAll("#regler .regler").length,
|
||
pegel: document.querySelectorAll("#regler .pegel").length,
|
||
knoepfe_sichtbar: ["p-vorbereiten", "p-starten", "p-beenden"]
|
||
.filter((id) => sicht("#" + id)).length,
|
||
/* WOHER KOMMT EIN UEBERLAUF? Die blosse Zahl „197 Pixel zum
|
||
Scrollen" schickt einen ins Raten. Die Hoehen daneben zeigen
|
||
in einer Zeile, welcher Kasten zu gross ist. */
|
||
hoehen: (() => {
|
||
const h = (w) => {
|
||
const e = document.querySelector(w);
|
||
return e ? Math.round(e.getBoundingClientRect().height) : -1;
|
||
};
|
||
return `fenster ${window.innerHeight} · saal ${h("#saal")}`
|
||
+ ` · kino ${h("#teil-live")} · pult ${h("#pult")}`
|
||
+ ` (leiste ${h(".pult__leiste")} + innen ${h("#pult-innen")})`
|
||
+ ` · tafeln sichtbar ${[...document.querySelectorAll(".pult-tafel")]
|
||
.filter((e) => e.getBoundingClientRect().height > 0).length}`
|
||
+ ` · innen rollt ${document.getElementById("pult-innen").scrollHeight}`;
|
||
})(),
|
||
kinder: [...document.getElementById("pult-innen").children].map((e) => {
|
||
const r = e.getBoundingClientRect();
|
||
const c = getComputedStyle(e);
|
||
return `${e.className || e.tagName}:${Math.round(r.top)}+${Math.round(r.height)}`
|
||
+ `/${c.display}/${c.position}`;
|
||
}).join(" "),
|
||
};
|
||
});
|
||
console.log(" " + regie.hoehen);
|
||
console.log(" " + regie.kinder);
|
||
console.log(` Lampe „${regie.lampe}" Uhr ${regie.uhr} `
|
||
+ `Sehen zu ${regie.dabei} mit Bild ${regie.sicht_zahl} Upload ${regie.last}`);
|
||
console.log(` Spur: „${regie.spur}" ${regie.zeit}`);
|
||
console.log(` ${regie.reiter} Register, ${regie.tafel_sichtbar} Tafel offen, `
|
||
+ `${regie.regler} Regler mit ${regie.pegel} Pegeln, `
|
||
+ `${regie.knoepfe_sichtbar}/3 grosse Knoepfe sichtbar`);
|
||
|
||
/* JEDE DIESER ZAHLEN KANN STILL FALSCH SEIN -- und genau dann sieht
|
||
ein Bildschirmfoto trotzdem gut aus. Deshalb werden sie geprueft
|
||
und nicht nur ausgegeben. */
|
||
if (regie.auf !== "ja") {
|
||
console.log(" ACHTUNG: Die Regie liess sich nicht aufklappen.");
|
||
ausgang = 1;
|
||
}
|
||
if (!/^\d+:\d\d/.test(regie.uhr) || regie.uhr === "0:00") {
|
||
console.log(` ACHTUNG: Die Laufzeit steht still (${regie.uhr}).`);
|
||
ausgang = 1;
|
||
}
|
||
if (!/kbit\/s|Mbit\/s/.test(regie.last)) {
|
||
console.log(` ACHTUNG: Der Upload wird nicht gemessen (${regie.last}).`);
|
||
ausgang = 1;
|
||
}
|
||
/* GEZAEHLT, NICHT FESTGESCHRIEBEN. Vorher stand hier „genau 5" --
|
||
und beim sechsten Register wurde die Messung rot, obwohl nichts
|
||
kaputt war. Was zaehlt, ist: zu jedem Reiter gehoert eine Tafel,
|
||
und genau eine ist offen. */
|
||
if (regie.reiter < 5 || regie.reiter !== regie.tafeln || regie.tafel_sichtbar !== 1) {
|
||
console.log(` ACHTUNG: Die Register stimmen nicht (${regie.reiter} Reiter, `
|
||
+ `${regie.tafeln} Tafeln, ${regie.tafel_sichtbar} offen).`);
|
||
ausgang = 1;
|
||
}
|
||
if (regie.regler !== 4 || regie.pegel !== 4 || regie.knoepfe_sichtbar !== 3) {
|
||
console.log(" ACHTUNG: Regler, Pegel oder die grossen Knoepfe fehlen.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ---- Kamera aus und Chat-Stufe ------------------------------------
|
||
Beides ist erst gebaut, wenn es beim ZUSCHAUER ankommt. */
|
||
console.log("");
|
||
console.log(" --- Kamera aus, Chat zu ---");
|
||
|
||
await host.seite.click("#s-kamera").catch(() => {});
|
||
await host.seite.waitForTimeout(1200);
|
||
const nachKamera = await zuschauer.seite.evaluate(() => {
|
||
const f = document.querySelector('#kameras .kamera[data-kamera="aus"]');
|
||
return {
|
||
schild: f ? f.querySelector(".kamera__aus")?.textContent?.trim() : null,
|
||
wessen: f ? f.querySelector(".kamera__name")?.textContent?.trim() : null,
|
||
videoVersteckt: f ? getComputedStyle(f.querySelector("video")).opacity === "0" : false,
|
||
};
|
||
});
|
||
console.log(` Zuschauerin sieht: „${nachKamera.schild || "(nichts)"}" `
|
||
+ `bei ${nachKamera.wessen || "(niemandem)"}, Bild verdeckt=${nachKamera.videoVersteckt}`);
|
||
if (nachKamera.schild !== "Kamera aus" || !nachKamera.videoVersteckt) {
|
||
console.log(" ACHTUNG: Das abgeschaltete Bild kommt nicht als Schild an.");
|
||
ausgang = 1;
|
||
}
|
||
await host.seite.click("#s-kamera").catch(() => {});
|
||
await host.seite.waitForTimeout(900);
|
||
|
||
await host.seite.click('#chat-stufen .chatstufe[data-modus="team"]').catch(() => {});
|
||
await host.seite.waitForTimeout(1200);
|
||
const nachChat = await zuschauer.seite.evaluate(() => {
|
||
const f = document.getElementById("chat-feld");
|
||
return { gesperrt: !!f?.disabled, grund: f?.placeholder || "",
|
||
stufe: document.getElementById("schiene")?.dataset.chat };
|
||
});
|
||
console.log(` Zuschauerin: Feld gesperrt=${nachChat.gesperrt} `
|
||
+ `Grund „${nachChat.grund}" Stufe=${nachChat.stufe}`);
|
||
/* GEGENPROBE AM SERVER. Ein ausgegrautes Feld haelt niemanden auf,
|
||
der die Schnittstelle kennt -- also wird auch dort nachgefragt. */
|
||
const trotzdem = await roh("/workspace/api/reaktion/chat", {
|
||
method: "POST", keks: k.gast, rumpf: { text: "geht das trotzdem?" },
|
||
});
|
||
console.log(` ... und der Server lehnt ab: ${trotzdem.code}/${trotzdem.j?.fehler}`);
|
||
if (!nachChat.gesperrt || !nachChat.grund.includes("Team") || trotzdem.code !== 403) {
|
||
console.log(" ACHTUNG: Die Chat-Stufe kommt nicht an.");
|
||
ausgang = 1;
|
||
}
|
||
await host.seite.click('#chat-stufen .chatstufe[data-modus="offen"]').catch(() => {});
|
||
await host.seite.waitForTimeout(900);
|
||
|
||
/* ---- Die Spenden -------------------------------------------------
|
||
Der ganze Weg: eintragen -> Karte laeuft bei allen. Melden ->
|
||
wartet. Bestaetigen -> laeuft. Und eine unbestaetigte Meldung
|
||
darf NICHT im Bild stehen. */
|
||
console.log("");
|
||
console.log(" --- Spenden ---");
|
||
|
||
/* Damit es Betragsknoepfe gibt, braucht die Unterstuetzen-Seite eine
|
||
PayPal.me-Adresse. Sie wird ueber den echten Weg gesetzt -- eine
|
||
Zeile direkt in die Datenbank zu schreiben wuerde genau die
|
||
Pruefungen umgehen, um die es geht. */
|
||
const wege = await roh("/workspace/api/unterstuetzung", { keks: k.admin });
|
||
const ppWeg = (wege.j?.wege || wege.j?.liste || []).find((w) => w.schluessel === "paypal");
|
||
if (ppWeg) {
|
||
await roh(`/workspace/api/unterstuetzung/${ppWeg.id}`, {
|
||
method: "PUT", keks: k.admin,
|
||
rumpf: {
|
||
ziel: "https://paypal.me/probehase", knopf: "Dogi was ausgeben", sichtbar: true,
|
||
},
|
||
});
|
||
}
|
||
const spLage = await roh("/workspace/api/spenden", { keks: k.gast });
|
||
console.log(` Betragsknoepfe: ${(spLage.j?.knoepfe || []).map((b) => b.ziel.split("/").pop()).join(", ") || "(keine)"}`);
|
||
if ((spLage.j?.knoepfe || []).length !== 4) {
|
||
console.log(" ACHTUNG: Die Betragsknoepfe fehlen.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* DIE ZUSCHAUERSEITE WIRD NEU GELADEN. Sie wurde geoeffnet, BEVOR
|
||
die PayPal-Adresse gesetzt war -- ohne sie gibt es keinen
|
||
Geben-Kasten, und der ist ab hier das Gemessene. Ein Neuladen ist
|
||
dabei nicht geschummelt, sondern der Normalfall: So kommt jeder
|
||
an, der spaeter dazustoesst. */
|
||
await zuschauer.seite.reload({ waitUntil: "domcontentloaded" });
|
||
await zuschauer.seite.waitForTimeout(1800);
|
||
|
||
/* 1. Von Hand eintragen -- die Karte muss bei der Zuschauerin laufen. */
|
||
await zuschauer.seite.evaluate(() => { window.__karten = []; });
|
||
await zuschauer.seite.evaluate(() => {
|
||
const t = document.getElementById("spendentafel");
|
||
new MutationObserver((m) => {
|
||
for (const e of m) {
|
||
for (const n of e.addedNodes) {
|
||
if (n.querySelector) {
|
||
window.__karten.push({
|
||
/* NUR DIE ZAHL, NICHT DIE GANZE ZEILE. Seit dem
|
||
28.09. steht neben ihr ein Zeichen und das Wort
|
||
„Dogen"; `textContent` klebte beides zusammen
|
||
(„2,5Dogen"), und eine Suche mit regulaerem Ausdruck
|
||
darin traefe frueher oder spaeter das Falsche. */
|
||
betrag: n.querySelector(".dogen__zahl")?.textContent?.trim(),
|
||
wort: n.querySelector(".dogen__wort")?.textContent?.trim(),
|
||
wer: n.querySelector(".spendenkarte__wer")?.textContent?.trim(),
|
||
stufe: n.querySelector(".spendenkarte__stufe")?.textContent?.trim(),
|
||
gruss: n.querySelector(".spendenkarte__gruss")?.textContent?.trim(),
|
||
/* Welche Muenze gross auf der Karte steht -- und ob die
|
||
kleine daneben weggenommen wurde. Dieselbe Muenze in
|
||
zwei Groessen auf einer Karte saehe aus wie ein
|
||
Versehen. */
|
||
muenze: (n.querySelector(".spendenkarte__bild img")
|
||
?.getAttribute("src") || "").split("/").pop(),
|
||
kleinWeg: n.dataset?.muenze === "ja",
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}).observe(t, { childList: true });
|
||
});
|
||
|
||
await roh("/workspace/api/spenden", {
|
||
method: "POST", keks: k.admin,
|
||
rumpf: { dogen_mille: 2500, name: "Nachtfalter", gruss: "Fuer das Rudel!" },
|
||
});
|
||
await zuschauer.seite.waitForFunction(
|
||
() => document.querySelector('.spendenkarte[data-da="ja"]'), null, { timeout: 8000 },
|
||
).catch(() => {});
|
||
await zuschauer.seite.waitForTimeout(700);
|
||
await bild(zuschauer.seite, "10-spendenkarte");
|
||
|
||
/* ==== 2. DER ZUSCHAUER GIBT UND SCHREIBT SELBST ==================
|
||
|
||
Filipe: „der text der dazu erscheint sollen die leute selber
|
||
schreiben koennen ... es sollen personalisierte texte sein von den
|
||
leuten selbst."
|
||
|
||
Gemessen wird der WEG DURCH DIE OBERFLAECHE und nicht der Aufruf
|
||
dahinter: Ein Knopf, den man drueckt, ein Feld, das man ausfuellt,
|
||
ein Text, der ankommt. Der Aufruf allein haette nie gezeigt, ob
|
||
das Feld ueberhaupt existiert.
|
||
|
||
Und dieselbe Meldung beantwortet die alte Frage gleich mit: Sie
|
||
ist UNBESTAETIGT und darf deshalb NICHT im Bild stehen. */
|
||
const geben = await zuschauer.seite.evaluate(() => {
|
||
const k2 = document.getElementById("geben");
|
||
const knoepfe = [...document.querySelectorAll(".geben__betrag")];
|
||
return {
|
||
da: k2 && !k2.hidden,
|
||
knoepfe: knoepfe.length,
|
||
zahlen: knoepfe.map((a) => a.querySelector(".dogen__zahl")?.textContent || "?"),
|
||
/* ==== DAS ZEICHEN MUSS DA SEIN -- UND GELADEN ==============
|
||
Seit dem 28.09. ist es ein BILD (Filipes Muenzen) und keine
|
||
Zeichnung mehr. Gezaehlt wird deshalb nicht, ob ein Knoten da
|
||
ist, sondern ob er etwas ZEIGT: Ein `img`, das 404 antwortet,
|
||
steht genauso im Dokument wie eines, das laedt -- und auf dem
|
||
Bildschirm ist an seiner Stelle nichts.
|
||
`naturalWidth > 0` ist der Unterschied, und er ist genau der,
|
||
den man sonst erst im Livestream sieht. */
|
||
zeichen: knoepfe.filter((a) => {
|
||
const b = a.querySelector(".dogen__zeichen");
|
||
if (!b) return false;
|
||
return b.tagName.toLowerCase() === "img" ? b.naturalWidth > 0 : true;
|
||
}).length,
|
||
/* ==== HIER MUSS DER PREIS STEHEN ==========================
|
||
Filipe: „die leute sollen nur sehen was dogen kosten. sonst
|
||
wissen die ja nicht wie viel es kosten wird."
|
||
Hier stand bis zum 28.09. das Gegenteil -- „nirgends ein
|
||
Euro". Das war fuer jede ANZEIGE richtig und fuer den einen
|
||
Kaufknopf falsch. */
|
||
preise: knoepfe.map((a) => a.querySelector(".geben__preis")?.textContent?.trim() || ""),
|
||
/* ==== UND SONST NIRGENDS AUF DER SEITE ====================
|
||
Die staerkste Form der Frage: den ganzen sichtbaren Text
|
||
nehmen, den Geben-Kasten herausschneiden und nachsehen, ob
|
||
irgendwo noch ein Eurozeichen uebrig ist. Eine Pruefung, die
|
||
nur den Kasten ansieht, uebersieht genau die Stelle, an der
|
||
es versehentlich landet. */
|
||
geldSonstwo: (() => {
|
||
const alles = document.body.textContent || "";
|
||
const kasten = k2?.textContent || "";
|
||
return /€|EUR/.test(alles.split(kasten).join(" "));
|
||
})(),
|
||
};
|
||
});
|
||
console.log(` Geben-Kasten: ${geben.knoepfe} Knoepfe (${geben.zahlen.join(" · ")}), `
|
||
+ `${geben.zeichen} mit Zeichen`);
|
||
console.log(` Preise am Knopf: ${geben.preise.join(" · ")} `
|
||
+ `| Geld sonstwo auf der Seite: ${geben.geldSonstwo}`);
|
||
if (!geben.da || geben.knoepfe !== 4 || geben.zeichen !== 4
|
||
|| geben.zahlen[0] !== "0,2") {
|
||
console.log(" ACHTUNG: Der Geben-Kasten zeigt nicht, was er soll.");
|
||
ausgang = 1;
|
||
}
|
||
/* WER BEZAHLT, MUSS VORHER WISSEN, WAS ER ZAHLT. Vier Knoepfe, vier
|
||
Preise -- ein Knopf ohne Preis waere der eine, bei dem es niemand
|
||
merkt. */
|
||
if (geben.preise.some((p) => !/\d/.test(p) || !p.includes("€"))) {
|
||
console.log(` ACHTUNG: An einem Knopf fehlt der Preis (${geben.preise.join(" | ")}).`);
|
||
ausgang = 1;
|
||
}
|
||
/* UND SONST NIRGENDS. Ein Zuschauer sieht den Preis dort, wo er
|
||
kauft -- und sonst nur Dogen. */
|
||
if (geben.geldSonstwo) {
|
||
console.log(" ACHTUNG: Auf der Zuschauerseite steht Geld ausserhalb der Kaufknoepfe.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ==== DIE PROBE AENDERT NUR DAS, WAS SIE AENDERN MUSS ============
|
||
Der Knopf ist ein Verweis mit `target="_blank"` und fuehrt zu
|
||
PayPal. Ein Klick in der Messung wuerde also ins Netz gehen. Ein
|
||
Zuhoerer in der EINFANGPHASE nimmt nur die Navigation weg -- der
|
||
Zuhoerer der Seite laeuft danach ganz normal.
|
||
|
||
Das ist die Lehre vom 28.09. in klein: Eine Probe, die mehr
|
||
aendert als das, wonach man sucht, beantwortet eine andere
|
||
Frage. Hier wird kein Ereignis nachgebaut und keine Funktion
|
||
direkt gerufen -- es wird wirklich geklickt. */
|
||
await zuschauer.seite.evaluate(() => {
|
||
document.addEventListener("click", (e) => {
|
||
if (e.target.closest && e.target.closest("a.geben__betrag")) e.preventDefault();
|
||
}, true);
|
||
});
|
||
await zuschauer.seite.click(".geben__betrag");
|
||
await zuschauer.seite.waitForTimeout(400);
|
||
|
||
const felder = await zuschauer.seite.evaluate(() => {
|
||
const name = document.getElementById("geben-name");
|
||
const text = document.getElementById("geben-text");
|
||
return {
|
||
offen: !document.getElementById("geben-nach").hidden,
|
||
frage: document.getElementById("geben-frage")?.textContent?.trim() || "",
|
||
knopf: document.getElementById("geben-melden")?.textContent?.trim() || "",
|
||
name: name?.value || "",
|
||
grenze: text?.maxLength || 0,
|
||
/* Am Finger muessen beide Felder gross genug sein. */
|
||
hoch: Math.round(text?.getBoundingClientRect().height || 0),
|
||
};
|
||
});
|
||
console.log(` Nach dem Tippen: „${felder.frage}" · Knopf „${felder.knopf}"`);
|
||
console.log(` Felder: Name „${felder.name}", Text bis ${felder.grenze} Zeichen, `
|
||
+ `${felder.hoch} px hoch`);
|
||
if (!felder.offen || felder.grenze !== 140 || felder.name !== "Nachtfalter"
|
||
|| /€|EUR/.test(felder.frage) || !felder.frage.includes("Dogen")) {
|
||
console.log(" ACHTUNG: Die Felder fuer den eigenen Text stimmen nicht.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* Der Zaehler steht erst da, wenn es eng wird -- vorher waere er
|
||
eine Aufsicht statt einer Hilfe. */
|
||
const kurz = await zuschauer.seite.evaluate(() => {
|
||
const t = document.getElementById("geben-text");
|
||
t.value = "Kurz";
|
||
t.dispatchEvent(new Event("input", { bubbles: true }));
|
||
return document.getElementById("geben-rest")?.textContent || "";
|
||
});
|
||
const lang = await zuschauer.seite.evaluate(() => {
|
||
const t = document.getElementById("geben-text");
|
||
t.value = "Danke fuer den Abend, ihr Lieben! Bleibt genau so wie ihr seid "
|
||
+ "-- das hier ist das Beste am ganzen Tag, ehrlich jetzt.";
|
||
t.dispatchEvent(new Event("input", { bubbles: true }));
|
||
return document.getElementById("geben-rest")?.textContent || "";
|
||
});
|
||
console.log(` Zaehler: bei „Kurz" „${kurz}", bei langem Text „${lang}"`);
|
||
if (kurz !== "" || !lang.startsWith("noch ")) {
|
||
console.log(" ACHTUNG: Der Zaehler erscheint zur falschen Zeit.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
await zuschauer.seite.click("#geben-melden");
|
||
await zuschauer.seite.waitForTimeout(1400);
|
||
const danach = await zuschauer.seite.evaluate(() => ({
|
||
danke: document.getElementById("geben-danke")?.textContent?.trim() || "",
|
||
offen: !document.getElementById("geben-nach").hidden,
|
||
leer: document.getElementById("geben-text")?.value === "",
|
||
}));
|
||
console.log(` Danach: „${danach.danke}"`);
|
||
if (danach.offen || !danach.leer || !danach.danke.includes("Text")) {
|
||
console.log(" ACHTUNG: Nach dem Melden stimmt die Antwort nicht.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
const nachMelden = await zuschauer.seite.evaluate(() => window.__karten);
|
||
console.log(` Karten bei der Zuschauerin: ${JSON.stringify(nachMelden)}`);
|
||
if (nachMelden.length !== 1 || nachMelden[0]?.betrag !== "2,5") {
|
||
console.log(" ACHTUNG: Die eingetragene Spende kommt nicht als Karte an.");
|
||
ausgang = 1;
|
||
}
|
||
if (nachMelden.some((k2) => k2.betrag === "0,2")) {
|
||
console.log(" ACHTUNG: Eine UNBESTAETIGTE Meldung stand im Bild.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* 3. Bestaetigen -- jetzt muss sie laufen. */
|
||
const offen = await roh("/workspace/api/spenden", { keks: k.admin });
|
||
const wartet = (offen.j?.offen || [])[0];
|
||
console.log(` Im Pult wartet: ${wartet ? wartet.dogen_mille / 1000 + " Dogen von " + wartet.name : "(nichts)"}`);
|
||
if (!wartet) {
|
||
console.log(" ACHTUNG: Die Meldung kommt nicht im Pult an.");
|
||
ausgang = 1;
|
||
} else {
|
||
/* ==== DIE SCHRANKE VOR DEM STREAM ============================
|
||
Der Text kommt von einem Fremden und steht gleich im Bild.
|
||
Geprueft wird deshalb, dass er (a) im Pult ANKOMMT, (b) sich
|
||
dort AENDERN laesst und (c) genau so auf der Karte landet. */
|
||
console.log(` Der Zuschauer schrieb: „${(wartet.gruss || "").slice(0, 60)}…"`);
|
||
if (!wartet.gruss || !wartet.gruss.startsWith("Danke fuer den Abend")) {
|
||
console.log(" ACHTUNG: Der selbst geschriebene Text kommt nicht im Pult an.");
|
||
ausgang = 1;
|
||
}
|
||
const inFeldern = await host.seite.evaluate(() => {
|
||
const z = document.querySelector("#sp-offen .spendenzeile");
|
||
const f = z ? [...z.querySelectorAll(".spendenzeile__feld")] : [];
|
||
return {
|
||
felder: f.length,
|
||
wert: f[1]?.value || "",
|
||
grenze: f[1]?.maxLength || 0,
|
||
zahl: z?.querySelector(".dogen__zahl")?.textContent || "",
|
||
};
|
||
});
|
||
console.log(` Im Pult: ${inFeldern.felder} Felder, Text bis ${inFeldern.grenze} `
|
||
+ `Zeichen, Zahl „${inFeldern.zahl}"`);
|
||
if (inFeldern.felder !== 2 || inFeldern.grenze !== 140
|
||
|| inFeldern.zahl !== "0,2") {
|
||
console.log(" ACHTUNG: Die Leitung kann den Text nicht sehen oder nicht aendern.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
await roh(`/workspace/api/spenden/${wartet.id}/bestaetigen`, {
|
||
method: "POST", keks: k.admin,
|
||
rumpf: { dogen_mille: 1000, gruss: "Danke fuer den Abend!" },
|
||
});
|
||
/* AUF DAS MERKMAL WARTEN, NICHT AUF DIE UHR. Die erste Karte
|
||
laeuft elf Sekunden (die hoechste Stufe steht am laengsten); die
|
||
zweite wartet in der Schlange, damit sie nicht uebereinander
|
||
liegen. Eine feste Pause haette das als Fehler gemeldet. */
|
||
await zuschauer.seite.waitForFunction(
|
||
() => (window.__karten || []).length >= 2, null, { timeout: 20000 },
|
||
).catch(() => {});
|
||
const nachJa = await zuschauer.seite.evaluate(() => window.__karten);
|
||
console.log(` Nach dem Bestaetigen: ${JSON.stringify(nachJa.slice(-1))}`);
|
||
if (nachJa.length !== 2 || nachJa[1]?.betrag !== "1") {
|
||
console.log(" ACHTUNG: Die bestaetigte Spende laeuft nicht -- oder mit falschem Betrag.");
|
||
ausgang = 1;
|
||
}
|
||
/* DIE STUFE MUSS ZUR ZAHL PASSEN. 2,5 Dogen ist die hoechste
|
||
(„Rudel-Legende"), 1 Doge die mittlere („Starke Runde"). Ohne
|
||
diese Probe koennte jede Karte dieselbe Stufe tragen und es
|
||
faellt niemandem auf. */
|
||
console.log(` Stufen: ${nachMelden[0]?.stufe} / ${nachJa[1]?.stufe}`);
|
||
/* EINZAHL UND MEHRZAHL. „1 Dogen" waere die Sorte Fehler, die
|
||
jeder sieht und niemand meldet. */
|
||
console.log(` Einheit: „${nachMelden[0]?.wort}" bei 2,5 · `
|
||
+ `„${nachJa[1]?.wort}" bei 1`);
|
||
if (nachMelden[0]?.wort !== "Dogen" || nachJa[1]?.wort !== "Doge") {
|
||
console.log(" ACHTUNG: Einzahl und Mehrzahl stimmen nicht.");
|
||
ausgang = 1;
|
||
}
|
||
/* Der Text, nicht das Bild: Grossbuchstaben macht das Stilblatt. */
|
||
if (nachMelden[0]?.stufe !== "Rudel-Legende" || nachJa[1]?.stufe !== "Starke Runde") {
|
||
console.log(" ACHTUNG: Die Stufe passt nicht zur Zahl.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ==== DER BERICHTIGTE TEXT STEHT AUF DER KARTE ================
|
||
Nicht der, den der Zuschauer geschrieben hat, sondern der, den
|
||
die Leitung freigegeben hat. Andersherum waere die ganze
|
||
Schranke wirkungslos -- und niemandem fiele es auf, bis es
|
||
einmal zaehlt. */
|
||
console.log(` Auf der Karte steht: „${nachJa[1]?.gruss}"`);
|
||
if (nachJa[1]?.gruss !== "Danke fuer den Abend!") {
|
||
console.log(" ACHTUNG: Der freigegebene Text steht nicht auf der Karte.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ==== UND DIE PUNKTE SIND SOFORT DA ===========================
|
||
Filipe: „ich will dass die den leuten auch als punkte
|
||
hinzugefuegt werden."
|
||
|
||
GEMESSEN BEIM ZUSCHAUER UND OHNE NEULADEN. Genau hier war ein
|
||
Loch: `spendenHolen()` ruft bei einer Zuschauerin niemand auf,
|
||
das tut nur der Host -- ihr Stand waere erst beim naechsten
|
||
Seitenaufruf gewachsen. „Hinzugefuegt werden" heisst sichtbar
|
||
hinzugefuegt. */
|
||
await zuschauer.seite.waitForFunction(
|
||
() => !document.getElementById("geben-stand")?.hidden, null, { timeout: 8000 },
|
||
).catch(() => {});
|
||
const stand = await zuschauer.seite.evaluate(() => {
|
||
const p = document.getElementById("geben-stand");
|
||
return {
|
||
da: !!p && !p.hidden,
|
||
zahl: p?.querySelector(".dogen__zahl")?.textContent || "",
|
||
wort: p?.querySelector(".dogen__wort")?.textContent || "",
|
||
zeichen: (() => {
|
||
const b = p?.querySelector(".dogen__zeichen");
|
||
if (!b) return false;
|
||
return b.tagName.toLowerCase() === "img" ? b.naturalWidth > 0 : true;
|
||
})(),
|
||
text: p?.textContent?.trim() || "",
|
||
};
|
||
});
|
||
console.log(` Eigener Stand ohne Neuladen: „${stand.text}" `
|
||
+ `(Zeichen: ${stand.zeichen})`);
|
||
if (!stand.da || stand.zahl !== "1" || stand.wort !== "Doge" || !stand.zeichen) {
|
||
console.log(" ACHTUNG: Der eigene Dogen-Stand kommt nicht an.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ==== DIE MUENZE MUSS ZUR HOEHE DER SPENDE PASSEN =============
|
||
|
||
Filipe: „drei varianten fuer niedrige spenden. mittlere und
|
||
hohe spenden. aber pass sie sofort diesen 3 kategorien an."
|
||
|
||
Die erste Karte sind 2,5 Dogen (Rudel-Legende, oberes Drittel),
|
||
die zweite 1 Doge (Starke Runde, mittleres). Stuende auf beiden
|
||
dieselbe Muenze, waere die ganze Einteilung wirkungslos -- und
|
||
das sieht man einer einzelnen Karte nie an. */
|
||
console.log(` Muenzen: „${nachMelden[0]?.muenze}" bei 2,5 · `
|
||
+ `„${nachJa[1]?.muenze}" bei 1`);
|
||
console.log(` Kleine Muenze daneben weggenommen: `
|
||
+ `${nachMelden[0]?.kleinWeg} / ${nachJa[1]?.kleinWeg}`);
|
||
if (!/-hoch\.webp$/.test(nachMelden[0]?.muenze || "")
|
||
|| !/-mittel\.webp$/.test(nachJa[1]?.muenze || "")) {
|
||
console.log(" ACHTUNG: Die Muenze passt nicht zur Hoehe der Spende.");
|
||
ausgang = 1;
|
||
}
|
||
if (!nachMelden[0]?.kleinWeg || !nachJa[1]?.kleinWeg) {
|
||
console.log(" ACHTUNG: Die Muenze steht zweimal auf derselben Karte.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ---- UND DER SATZ LAESST SICH WECHSELN ----
|
||
Gemessen ueber die Probe beim Host: Sie schreibt nichts in die
|
||
Datenbank und ist damit der einzige Weg, eine Karte zu sehen,
|
||
ohne eine Spende zu erfinden. */
|
||
await roh("/workspace/api/spenden/zeichen/satz", {
|
||
method: "PUT", keks: k.admin, rumpf: { satz: "neon" },
|
||
});
|
||
await host.seite.waitForTimeout(700);
|
||
await roh("/workspace/api/spenden/probe", {
|
||
method: "POST", keks: k.admin, rumpf: { dogen_mille: 2500 },
|
||
});
|
||
/* AUF DIE PROBEKARTE WARTEN, NICHT AUF IRGENDEINE.
|
||
|
||
Beim ersten Lauf stand noch die vorige Karte („Starke Runde",
|
||
acht Sekunden) auf dem Bildschirm, und die Messung las deren
|
||
Muenze -- sie meldete „klassik-mittel" und damit einen Fehler,
|
||
den es nicht gab. Die Karten laufen in einer SCHLANGE: Die
|
||
naechste kommt erst, wenn die vorige abgelaufen ist. Also auf
|
||
das Merkmal warten, das nur die neue hat. */
|
||
await host.seite.waitForFunction(
|
||
() => [...document.querySelectorAll('.spendenkarte[data-da="ja"]')].some(
|
||
(k2) => k2.querySelector(".spendenkarte__wer")?.textContent === "Probe"
|
||
&& k2.querySelector(".spendenkarte__bild img")),
|
||
null, { timeout: 20000 },
|
||
).catch(() => {});
|
||
const nachWechsel = await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll('.spendenkarte[data-da="ja"]')]
|
||
.find((x) => x.querySelector(".spendenkarte__wer")?.textContent === "Probe");
|
||
const b = k2?.querySelector(".spendenkarte__bild img");
|
||
/* Und auch hier: geladen, nicht nur vorhanden. */
|
||
return b && b.naturalWidth > 0
|
||
? (b.getAttribute("src") || "").split("/").pop() : "(nicht geladen)";
|
||
});
|
||
console.log(` Nach dem Wechsel auf „Neon": „${nachWechsel}"`);
|
||
if (nachWechsel !== "neon-hoch.webp") {
|
||
console.log(" ACHTUNG: Der gewechselte Muenzsatz kommt nicht an.");
|
||
ausgang = 1;
|
||
}
|
||
await roh("/workspace/api/spenden/zeichen/satz", {
|
||
method: "PUT", keks: k.admin, rumpf: { satz: "klassik" },
|
||
});
|
||
}
|
||
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="spenden"]').catch(() => {});
|
||
await host.seite.waitForTimeout(700);
|
||
await bild(host.seite, "9-live-regie-spenden");
|
||
|
||
await vermessen(host.seite, "Host am Rechner");
|
||
await bild(host.seite, "3-live-host");
|
||
|
||
/* Und einmal die Warteschlange -- das ist der Teil, den Filipe
|
||
vorher benutzt, nicht waehrend der Sendung. */
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="liste"]').catch(() => {});
|
||
await host.seite.waitForTimeout(600);
|
||
const warte = await host.seite.evaluate(() => ({
|
||
zeilen: document.querySelectorAll("#warteliste .warte-zeile").length,
|
||
bilder: [...document.querySelectorAll(".warte-zeile__bild")]
|
||
.filter((b) => b.naturalWidth > 0).length,
|
||
titel: [...document.querySelectorAll(".warte-zeile__titel")]
|
||
.map((e) => e.textContent.trim()).join(" | "),
|
||
naechstes: document.getElementById("v-naechstes")?.disabled,
|
||
}));
|
||
console.log(` Warteschlange: ${warte.zeilen} Zeilen, `
|
||
+ `${warte.bilder} Vorschaubilder geladen, „Naechstes" `
|
||
+ `${warte.naechstes ? "grau" : "nutzbar"}`);
|
||
console.log(` ${warte.titel}`);
|
||
if (warte.zeilen !== 2 || warte.naechstes) {
|
||
console.log(" ACHTUNG: Die Warteschlange kommt nicht an.");
|
||
ausgang = 1;
|
||
}
|
||
await bild(host.seite, "7-live-regie-liste");
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="ton"]').catch(() => {});
|
||
await host.seite.waitForTimeout(500);
|
||
await bild(host.seite, "8-live-regie-ton");
|
||
if (host.konsole.length) console.log(" KONSOLE:", host.konsole.slice(0, 4).join(" | "));
|
||
|
||
await vermessen(zuschauer.seite, "Zuschauerin am Rechner");
|
||
await bild(zuschauer.seite, "4-live-zuschauer");
|
||
if (zuschauer.konsole.length) console.log(" KONSOLE:", zuschauer.konsole.slice(0, 4).join(" | "));
|
||
|
||
/* ---- Rollen und Moderation im Chat --------------------------------
|
||
|
||
Filipe: „die modis, linke hand und rechte hand soll im chat extra
|
||
aussehen und auch sachen wie stummschaltungen, sperrungen,
|
||
meldunge ... machen koennen."
|
||
|
||
GEMESSEN UND NICHT HERGELEITET. Die Schiene ist die schmalste
|
||
Flaeche der ganzen Seite, und in jede Beitragszeile ist gerade ein
|
||
Abzeichen UND ein zweiter Knopf dazugekommen. Genau so ist am
|
||
28.09. die Regie 197 px uebergelaufen: Die Rechnung ging auf, die
|
||
Wirklichkeit nicht. */
|
||
/* ---- Groesse und Ecke der Kamerafenster -------------------------
|
||
|
||
Filipe: „und danach perfektionierst du auch die verschiedenen
|
||
groessen von kamera und so."
|
||
|
||
GEMESSEN WIRD BEIM ZUSCHAUER. Dass in der Datenbank 1,5 steht,
|
||
sagt nichts darueber, ob das Fenster bei den anderen groesser
|
||
wird -- dazwischen liegen ein Rundruf, eine Variable im Stilblatt
|
||
und eine Rechnung mit `clamp`. */
|
||
console.log("");
|
||
console.log(" --- Kameragroesse und Ecke ---");
|
||
{
|
||
/* Sauberer Anfang: „Video gross", damit die Ecke ueberhaupt gilt. */
|
||
await roh("/workspace/api/reaktion/layout", {
|
||
method: "POST", keks: k.admin, rumpf: { layout: "kino" },
|
||
});
|
||
await zuschauer.seite.waitForTimeout(900);
|
||
|
||
const messen = () => zuschauer.seite.evaluate(() => {
|
||
const kino = document.getElementById("teil-live");
|
||
const kk = document.querySelector(".kameras");
|
||
const ein = document.querySelector(".kamera");
|
||
const lein = document.querySelector(".leinwand");
|
||
if (!kk || !ein || !lein) return null;
|
||
const r = ein.getBoundingClientRect();
|
||
const lr = lein.getBoundingClientRect();
|
||
const fenster = [...document.querySelectorAll(".kamera")];
|
||
return {
|
||
gross: kino.style.getPropertyValue("--kam-gross") || "(?)",
|
||
ecke: kino.dataset.ecke || "(?)",
|
||
breit: Math.round(r.width),
|
||
/* Wo sitzt der Stapel? Als Abstand zu den vier Kanten der
|
||
Leinwand -- daran erkennt man die Ecke ohne Vorwissen. */
|
||
links: Math.round(kk.getBoundingClientRect().left - lr.left),
|
||
rechts: Math.round(lr.right - kk.getBoundingClientRect().right),
|
||
oben: Math.round(kk.getBoundingClientRect().top - lr.top),
|
||
unten: Math.round(lr.bottom - kk.getBoundingClientRect().bottom),
|
||
/* Und liegt noch alles IN der Leinwand? Sie hat
|
||
`overflow: hidden` -- was daneben liegt, ist unsichtbar,
|
||
nicht abgeschnitten sichtbar. */
|
||
drin: fenster.filter((f) => {
|
||
const q = f.getBoundingClientRect();
|
||
return q.left >= lr.left - 1 && q.right <= lr.right + 1
|
||
&& q.top >= lr.top - 1 && q.bottom <= lr.bottom + 1;
|
||
}).length,
|
||
fenster: fenster.length,
|
||
leinwand: Math.round(lr.width),
|
||
};
|
||
});
|
||
|
||
const normal = await messen();
|
||
console.log(` 1×: Fenster ${normal?.breit} px, Ecke ${normal?.ecke}, `
|
||
+ `Abstände l/r/o/u ${normal?.links}/${normal?.rechts}/${normal?.oben}/${normal?.unten}, `
|
||
+ `${normal?.drin}/${normal?.fenster} im Bild`);
|
||
|
||
/* ---- DIE GROESSE MUSS WIRKEN ---- */
|
||
const { KAMERA_GROESSEN } = await import("./reaktion-tabellen.js");
|
||
const gross = KAMERA_GROESSEN[KAMERA_GROESSEN.length - 1];
|
||
await roh("/workspace/api/reaktion/layout", {
|
||
method: "POST", keks: k.admin, rumpf: { kamera_gross: gross },
|
||
});
|
||
await zuschauer.seite.waitForTimeout(900);
|
||
const dick = await messen();
|
||
console.log(` ${gross}×: Fenster ${dick?.breit} px, `
|
||
+ `${dick?.drin}/${dick?.fenster} im Bild (Leinwand ${dick?.leinwand} px)`);
|
||
|
||
if (!normal || !dick) {
|
||
console.log(" ACHTUNG: Die Kamerafenster liessen sich nicht messen.");
|
||
ausgang = 1;
|
||
} else {
|
||
/* NICHT AUF DEN FAKTOR GENAU. `clamp` deckelt die Grundbreite,
|
||
und `max-width: 46%` deckelt den Stapel -- beides ist Absicht.
|
||
Geprueft wird, dass es SPUERBAR waechst und im Bild bleibt. */
|
||
const wuchs = dick.breit / normal.breit;
|
||
console.log(` Das Fenster wuchs um das ${wuchs.toFixed(2)}-fache`);
|
||
if (wuchs < 1.3) {
|
||
console.log(" ACHTUNG: Die Groesse wirkt nicht.");
|
||
ausgang = 1;
|
||
}
|
||
if (dick.drin !== dick.fenster) {
|
||
console.log(" ACHTUNG: Ein Fenster liegt ausserhalb der Leinwand.");
|
||
ausgang = 1;
|
||
}
|
||
if (dick.breit > dick.leinwand * 0.5) {
|
||
console.log(" ACHTUNG: Ein Fenster deckt mehr als die halbe Leinwand zu.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* ---- DIE VIER ECKEN ----
|
||
Geprueft wird der ABSTAND zu den Kanten, nicht der Name der
|
||
Ecke. Eine Einstellung, die nur ihren eigenen Namen bestaetigt,
|
||
bestaetigt nichts. */
|
||
await roh("/workspace/api/reaktion/layout", {
|
||
method: "POST", keks: k.admin, rumpf: { kamera_gross: 1 },
|
||
});
|
||
const erwartet = {
|
||
ol: ["links", "oben"], or: ["rechts", "oben"],
|
||
ul: ["links", "unten"], ur: ["rechts", "unten"],
|
||
};
|
||
for (const [ecke, [waag, senk]] of Object.entries(erwartet)) {
|
||
await roh("/workspace/api/reaktion/layout", {
|
||
method: "POST", keks: k.admin, rumpf: { kamera_ecke: ecke },
|
||
});
|
||
await zuschauer.seite.waitForTimeout(800);
|
||
const m = await messen();
|
||
const nahWaag = m[waag] < (waag === "links" ? m.rechts : m.links);
|
||
const nahSenk = m[senk] < (senk === "oben" ? m.unten : m.oben);
|
||
console.log(` Ecke ${ecke}: l/r/o/u ${m.links}/${m.rechts}/${m.oben}/${m.unten} `
|
||
+ `-> ${waag}+${senk} ${nahWaag && nahSenk ? "stimmt" : "STIMMT NICHT"}`);
|
||
if (!nahWaag || !nahSenk || m.ecke !== ecke) {
|
||
console.log(` ACHTUNG: Die Ecke „${ecke}" kommt nicht an.`);
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
await bild(zuschauer.seite, "22-kamera-ecke");
|
||
|
||
/* ---- EIN KNOPF, DER NICHTS TUN KANN, IST GRAU ----
|
||
Die Ecke wirkt nur bei „Video gross"; die Groesse wirkt ueberall,
|
||
wo es Kamerafenster GIBT -- bei „Nur Video" gibt es keine.
|
||
|
||
GEMESSEN WIRD DER ZUSTAND DER KNOEPFE, nicht der Satz darunter.
|
||
Ein Hinweis, der die Wahrheit sagt, waehrend der Knopf daneben
|
||
bedienbar bleibt, ist die schlechtere Haelfte: Man liest ihn
|
||
nicht und drueckt trotzdem. */
|
||
const knoepfe = () => host.seite.evaluate(() => {
|
||
const e = [...document.querySelectorAll("#kamecke .kamecke__knopf")];
|
||
const g = [...document.querySelectorAll("#kamgroesse .kamgroesse__knopf")];
|
||
return {
|
||
ecken: e.length, eckenGrau: e.filter((x) => x.disabled).length,
|
||
groessen: g.length, groessenGrau: g.filter((x) => x.disabled).length,
|
||
hinweis: document.getElementById("kamecke-hinweis")?.textContent || "",
|
||
};
|
||
});
|
||
|
||
for (const [anordnung, sollEcken, sollGroessen, wort] of [
|
||
["kamera", "alle", "keine", "Mitte"],
|
||
["nur_video", "alle", "alle", "Nur Video"],
|
||
["kino", "keine", "keine", "Ecke"],
|
||
]) {
|
||
await roh("/workspace/api/reaktion/layout", {
|
||
method: "POST", keks: k.admin, rumpf: { layout: anordnung },
|
||
});
|
||
await host.seite.waitForTimeout(800);
|
||
const m = await knoepfe();
|
||
console.log(` „${anordnung}": ${m.eckenGrau}/${m.ecken} Ecken grau, `
|
||
+ `${m.groessenGrau}/${m.groessen} Größen grau`);
|
||
console.log(` Hinweis: „${m.hinweis.replace(/\s+/g, " ").trim()}"`);
|
||
const soll = (art, wieviel, da) => (wieviel === "alle" ? art === da : art === 0);
|
||
if (m.ecken !== 4 || m.groessen < 4) {
|
||
console.log(" ACHTUNG: Die Knoepfe fuer Groesse oder Ecke fehlen.");
|
||
ausgang = 1;
|
||
} else if (!soll(m.eckenGrau, sollEcken, m.ecken)
|
||
|| !soll(m.groessenGrau, sollGroessen, m.groessen)) {
|
||
console.log(` ACHTUNG: Bei „${anordnung}" stimmt das Grauwerden nicht.`);
|
||
ausgang = 1;
|
||
} else if (!m.hinweis.includes(wort)) {
|
||
console.log(` ACHTUNG: Der Hinweis sagt nichts von „${wort}".`);
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
await roh("/workspace/api/reaktion/layout", {
|
||
method: "POST", keks: k.admin, rumpf: { layout: "kino", kamera_ecke: "ur" },
|
||
});
|
||
await host.seite.waitForTimeout(700);
|
||
}
|
||
|
||
/* ---- Die Regieleiste ueber dem Saal -----------------------------
|
||
|
||
Filipe, 28.09.2026: „ich haette das gerne oben also nicht in dieser
|
||
kachel sondern ueber der kachel wo die videos laufen werden."
|
||
|
||
GEMESSEN WIRD, WAS SIE KOSTET UND WAS SIE KANN. Eine Leiste am
|
||
oberen Rand nimmt Hoehe -- und zwar dem Video, um das es geht.
|
||
Zwei Zeilen statt einer waeren rund 90 Pixel, und das sieht man
|
||
auf dem Bildschirmfoto nicht, weil beides „eine Leiste" ist. */
|
||
console.log("");
|
||
console.log(" --- Die Regieleiste ---");
|
||
{
|
||
const leiste = await host.seite.evaluate(() => {
|
||
const l = document.getElementById("regieleiste");
|
||
const r = document.getElementById("reiter");
|
||
const kino = document.getElementById("teil-live");
|
||
if (!l || !r || !kino) return null;
|
||
const lr = l.getBoundingClientRect();
|
||
const kr = kino.getBoundingClientRect();
|
||
const knoepfe = [...r.querySelectorAll(".reiter__knopf")];
|
||
return {
|
||
hoch: Math.round(lr.height),
|
||
oben: Math.round(lr.top),
|
||
/* STEHT SIE WIRKLICH UEBER DEM KINO? Das ist die Frage, nicht
|
||
ob sie irgendwo existiert. */
|
||
ueberDemKino: lr.bottom <= kr.top + 1,
|
||
knoepfe: knoepfe.length,
|
||
/* EINE ZEILE ODER ZWEI? Gemessen an den Oberkanten -- wenn
|
||
zwei Knoepfe verschieden hoch sitzen, bricht sie um. */
|
||
zeilen: new Set(knoepfe.map((k) => Math.round(k.getBoundingClientRect().top))).size,
|
||
kleinster: Math.min(...knoepfe.map((k) => Math.round(k.getBoundingClientRect().height))),
|
||
rollt: r.scrollWidth > r.clientWidth,
|
||
/* Genau EIN Register in der Tabulatorfolge. */
|
||
imTabulator: knoepfe.filter((k) => k.tabIndex === 0).length,
|
||
marke: (() => {
|
||
const m = document.getElementById("reiter-marke");
|
||
if (!m || m.hidden) return null;
|
||
const mr = m.getBoundingClientRect();
|
||
const a = knoepfe.find((k) => k.getAttribute("aria-selected") === "true");
|
||
const ar = a.getBoundingClientRect();
|
||
return {
|
||
breit: Math.round(mr.width),
|
||
/* Sitzt sie unter dem gewaehlten Register? */
|
||
versatz: Math.round(Math.abs(mr.left - ar.left)),
|
||
gleichBreit: Math.abs(mr.width - ar.width) < 2,
|
||
};
|
||
})(),
|
||
};
|
||
});
|
||
|
||
if (!leiste) {
|
||
console.log(" ACHTUNG: Die Regieleiste gibt es nicht.");
|
||
ausgang = 1;
|
||
} else {
|
||
console.log(` ${leiste.knoepfe} Register in ${leiste.zeilen} Zeile(n), `
|
||
+ `Leiste ${leiste.hoch} px hoch bei y=${leiste.oben}, `
|
||
+ `kleinstes Ziel ${leiste.kleinster} px, rollt=${leiste.rollt}`);
|
||
console.log(` Ueber dem Kino: ${leiste.ueberDemKino} · `
|
||
+ `im Tabulator: ${leiste.imTabulator} · `
|
||
+ `Marke ${leiste.marke ? `${leiste.marke.breit} px, Versatz ${leiste.marke.versatz} px` : "(keine)"}`);
|
||
|
||
if (!leiste.ueberDemKino) {
|
||
console.log(" ACHTUNG: Die Leiste steht nicht ueber dem Kino.");
|
||
ausgang = 1;
|
||
}
|
||
if (leiste.zeilen !== 1) {
|
||
console.log(" ACHTUNG: Die Register brechen um -- das kostet das Video Hoehe.");
|
||
ausgang = 1;
|
||
}
|
||
if (leiste.kleinster < 44) {
|
||
console.log(" ACHTUNG: Ein Register ist kleiner als 44 px.");
|
||
ausgang = 1;
|
||
}
|
||
if (leiste.imTabulator !== 1) {
|
||
console.log(" ACHTUNG: Der Tabulator wandert nicht -- sieben Schritte bis zum Inhalt.");
|
||
ausgang = 1;
|
||
}
|
||
if (!leiste.marke || leiste.marke.versatz > 2 || !leiste.marke.gleichBreit) {
|
||
console.log(" ACHTUNG: Die Marke sitzt nicht unter dem gewaehlten Register.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* ---- DIE TASTATUR MUSS TUN, WAS `role=tablist` VERSPRICHT ----
|
||
Ein Versprechen im Markup, das das Programm nicht einloest, ist
|
||
schlimmer als gar keines: Der Vorleser sagt etwas an, was nicht
|
||
stimmt. Also wird es gedrueckt, nicht gelesen. */
|
||
await host.seite.evaluate(() => {
|
||
document.querySelector('#reiter .reiter__knopf[data-tafel="sendung"]').focus();
|
||
});
|
||
const tasten = [];
|
||
for (const [taste, wohin] of [["ArrowRight", "rechts"], ["ArrowRight", "rechts"],
|
||
["ArrowLeft", "links"], ["End", "ans Ende"], ["Home", "an den Anfang"]]) {
|
||
await host.seite.keyboard.press(taste);
|
||
await host.seite.waitForTimeout(220);
|
||
const wo = await host.seite.evaluate(() => {
|
||
const a = document.activeElement;
|
||
const g = [...document.querySelectorAll("#reiter .reiter__knopf")]
|
||
.find((k) => k.getAttribute("aria-selected") === "true");
|
||
const t = [...document.querySelectorAll(".pult-tafel")].find((x) => !x.hidden);
|
||
return {
|
||
fokus: a?.dataset?.tafel || "(?)",
|
||
gewaehlt: g?.dataset?.tafel || "(?)",
|
||
tafel: t?.dataset?.tafel || "(keine)",
|
||
};
|
||
});
|
||
tasten.push(`${wohin}->${wo.fokus}`);
|
||
/* FOKUS, AUSWAHL UND OFFENE TAFEL MUESSEN DASSELBE SAGEN.
|
||
Laufen sie auseinander, zeigt die Regie eine andere Tafel als
|
||
das hervorgehobene Register -- und man stellt etwas ein, von
|
||
dem man glaubt, es gehoere woandershin. */
|
||
if (wo.fokus !== wo.gewaehlt || wo.gewaehlt !== wo.tafel) {
|
||
console.log(` ACHTUNG: Fokus ${wo.fokus}, gewaehlt ${wo.gewaehlt}, `
|
||
+ `offen ${wo.tafel} -- das passt nicht zusammen.`);
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
console.log(` Tastatur: ${tasten.join(" · ")}`);
|
||
|
||
/* ---- EIN REGISTER MACHT DIE ZUGEKLAPPTE REGIE AUF ----
|
||
Seit die Leiste oben steht, kann das Pult zu sein, waehrend man
|
||
ein Register waehlt. Ohne das waere der Knopf einer, bei dem
|
||
sichtbar nichts passiert. */
|
||
await host.seite.click("#pult-griff").catch(() => { });
|
||
await host.seite.waitForTimeout(400);
|
||
const zu = await host.seite.evaluate(() =>
|
||
document.getElementById("pult").dataset.auf);
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="ton"]').catch(() => { });
|
||
await host.seite.waitForTimeout(500);
|
||
const danach = await host.seite.evaluate(() => ({
|
||
auf: document.getElementById("pult").dataset.auf,
|
||
tafel: [...document.querySelectorAll(".pult-tafel")].find((x) => !x.hidden)?.dataset.tafel,
|
||
sichtbar: (() => {
|
||
const t = document.getElementById("tafel-ton");
|
||
const r = t.getBoundingClientRect();
|
||
return r.height > 0 && r.top < window.innerHeight;
|
||
})(),
|
||
}));
|
||
console.log(` Regie zugeklappt (auf=${zu}), Register „Ton" gedrueckt `
|
||
+ `-> auf=${danach.auf}, offen=${danach.tafel}, sichtbar=${danach.sichtbar}`);
|
||
if (zu !== "nein" || danach.auf !== "ja" || !danach.sichtbar) {
|
||
console.log(" ACHTUNG: Ein Register macht die zugeklappte Regie nicht auf.");
|
||
ausgang = 1;
|
||
}
|
||
await bild(host.seite, "20-regieleiste");
|
||
|
||
/* ---- WAS SIE DEM VIDEO KOSTET ----
|
||
Die eigentliche Frage bei einer Leiste am oberen Rand. */
|
||
const platz = await host.seite.evaluate(() => {
|
||
const l = document.getElementById("regieleiste").getBoundingClientRect();
|
||
const k = document.getElementById("teil-live").getBoundingClientRect();
|
||
const s2 = document.getElementById("saal").getBoundingClientRect();
|
||
return {
|
||
leiste: Math.round(l.height), kino: Math.round(k.height), saal: Math.round(s2.height),
|
||
ueber: Math.max(0, document.documentElement.scrollHeight
|
||
- document.documentElement.clientHeight),
|
||
};
|
||
});
|
||
console.log(` Saal ${platz.saal} px = Leiste ${platz.leiste} + Kino ${platz.kino} `
|
||
+ `+ Regie, senkrecht ueber ${platz.ueber} px`);
|
||
if (platz.ueber > 6 || platz.kino < 200) {
|
||
console.log(" ACHTUNG: Die Leiste nimmt dem Bild zu viel oder die Seite laeuft ueber.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* ---- Die Gestaltung der Spendenkarten ---------------------------
|
||
|
||
Filipe: „mach paar fertige und so dass ich auch hochladen kann.
|
||
auch so dass ich das anders gestalten kann oder die größe
|
||
verändern kann."
|
||
|
||
GEMESSEN WIRD DIE KARTE, NICHT DIE EINSTELLUNG. Dass in der
|
||
Datenbank 2.0 steht, sagt nichts darüber, ob die Karte doppelt so
|
||
groß erscheint -- dazwischen liegen eine Zahl im Stilblatt, eine
|
||
Umrechnung in `em` und ein Dutzend Regeln, die noch in `rem`
|
||
stehen könnten. Also: Karte laufen lassen und nachmessen. */
|
||
console.log("");
|
||
console.log(" --- Die Gestaltung der Spendenkarten ---");
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="gestaltung"]').catch(() => {});
|
||
await host.seite.waitForTimeout(700);
|
||
|
||
const leiter = await host.seite.evaluate(() => {
|
||
const zeilen = [...document.querySelectorAll("#stufen .stufe-zeile")];
|
||
return {
|
||
zeilen: zeilen.length,
|
||
ersteName: zeilen[0]?.querySelector(".stufe-name")?.value || "",
|
||
felder: zeilen[0] ? [...zeilen[0].querySelectorAll("input, select")].length : 0,
|
||
/* Jedes Bedienelement muss mit dem Finger zu treffen sein --
|
||
hier wird eingestellt, was vor Publikum im Bild steht. */
|
||
/* VERSTECKTES ZAEHLT NICHT. Hinter jedem „Bild hochladen" liegt
|
||
ein `<input type="file" hidden>` -- es ist 0 px hoch, und das
|
||
ist richtig so: Angefasst wird das Schild davor. Die erste
|
||
Fassung hat es mitgezaehlt und drei Fehler gemeldet, die
|
||
keine waren. Eine Messung, die Unsichtbares misst, misst das
|
||
Falsche. */
|
||
zuKlein: zeilen.flatMap((z) => [...z.querySelectorAll("input, select, .stufe-tun")])
|
||
.filter((e) => !e.hidden && e.offsetParent !== null)
|
||
.filter((e) => e.getBoundingClientRect().height < 40).length,
|
||
ueber: Math.max(0, document.documentElement.scrollWidth
|
||
- document.documentElement.clientWidth),
|
||
};
|
||
});
|
||
console.log(` ${leiter.zeilen} Stufen, erste „${leiter.ersteName}", `
|
||
+ `${leiter.felder} Felder je Zeile, ${leiter.zuKlein} zu kleine Ziele, `
|
||
+ `seitlich ${leiter.ueber} px`);
|
||
if (leiter.zeilen < 3 || leiter.felder < 6 || leiter.zuKlein > 0 || leiter.ueber > 0) {
|
||
console.log(" ACHTUNG: Die Stufenleiter ist unvollstaendig oder laeuft ueber.");
|
||
ausgang = 1;
|
||
}
|
||
await bild(host.seite, "17-stufenleiter");
|
||
|
||
/* ---- Eine Stufe umgestalten und nachsehen, ob es ankommt ---- */
|
||
const vorherKarte = await host.seite.evaluate(async () => {
|
||
/* Erst einmal bei Größe 1 messen -- ohne Bezugswert sagt die zweite
|
||
Messung nichts. */
|
||
const k2 = [...document.querySelectorAll("#stufen .stufe-zeile")][2];
|
||
k2.querySelector(".stufe-groesse").value = "1";
|
||
k2.querySelector(".stufe-groesse").dispatchEvent(new Event("input", { bubbles: true }));
|
||
document.getElementById("stufen-sichern").click();
|
||
return true;
|
||
});
|
||
await host.seite.waitForTimeout(1200);
|
||
|
||
/* ==== ERST AUFRAEUMEN, DANN MESSEN ================================
|
||
|
||
Die erste Fassung hat einfach `document.querySelector(".spenden-
|
||
karte")` genommen -- und damit die Karte der echten 10-Euro-Spende
|
||
erwischt, die aus dem vorigen Messabschnitt noch im Bild stand.
|
||
Ergebnis: drei gemeldete Fehler, die keine waren, darunter „die
|
||
Probe laeuft im ganzen Saal". Eine Messung, die nicht weiss, WELCHE
|
||
Karte sie vor sich hat, misst irgendetwas.
|
||
|
||
Also: beide Tafeln leeren, dann druecken, dann auf eine Karte
|
||
warten, die sich als Probe ausweist. */
|
||
const tafelnLeeren = async () => {
|
||
for (const s2 of [host.seite, zuschauer.seite]) {
|
||
await s2.evaluate(() => {
|
||
for (const k2 of document.querySelectorAll(".spendenkarte")) k2.remove();
|
||
});
|
||
}
|
||
};
|
||
|
||
const messeKarte = async (erwartet) => {
|
||
await tafelnLeeren();
|
||
await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll("#stufen .stufe-zeile")][2];
|
||
[...k2.querySelectorAll(".stufe-tun")].find((x) => x.textContent === "Ausprobieren")?.click();
|
||
});
|
||
/* ==== AUF DIE RICHTIGE KARTE WARTEN, NICHT AUF DIE UHR ==========
|
||
|
||
Zwei Fallen auf einmal, und die zweite hat die erste Fassung
|
||
gekostet:
|
||
|
||
(1) Die Karten laufen in einer SCHLANGE. Ein Knoten aus dem
|
||
Dokument zu nehmen beendet nicht, was die Seite gerade
|
||
zeigt -- die naechste Karte kommt erst, wenn die Dauer der
|
||
vorigen abgelaufen ist. Bei elf Sekunden Dauer und acht
|
||
Sekunden Wartezeit kam sie nie, und die Messung meldete
|
||
einen Fehler, den es nicht gab.
|
||
|
||
(2) Gewartet wird auf die Karte MIT DER ERWARTETEN GROESSE.
|
||
Sonst koennte die vorige Probe die Antwort geben, und die
|
||
Messung bestaetigte eine Aenderung, die gar nicht
|
||
angekommen ist. */
|
||
await host.seite.waitForFunction(
|
||
(soll) => [...document.querySelectorAll('.spendenkarte[data-da="ja"]')].some((k2) =>
|
||
k2.querySelector(".spendenkarte__wer")?.textContent === "Probe"
|
||
&& Math.abs(parseFloat(getComputedStyle(k2).fontSize) - soll) < 0.5),
|
||
erwartet, { timeout: 16000 },
|
||
).catch(() => {});
|
||
/* DIE KARTE KOMMT VON LINKS HEREIN (450 ms). Wer sofort misst,
|
||
misst einen Zwischenschritt: Waehrend des Hereinkommens steht
|
||
sie 28 px weiter links und ist halb durchsichtig. Ein
|
||
Bildschirmfoto aus dieser Phase sieht aus, als waere sie
|
||
abgeschnitten -- und genau das hat mich beim ersten Ansehen in
|
||
die Irre gefuehrt. */
|
||
await host.seite.waitForTimeout(700);
|
||
return host.seite.evaluate(() => {
|
||
/* `data-da="ja"` HEISST: GANZ DA. Beim Hereinkommen UND beim
|
||
Verschwinden steht die Karte auf `translateX(-28px) scale(.96)`
|
||
-- gemessen wurden dadurch 413 statt 430 Pixel (genau das
|
||
0,96-fache) und ein linker Rand von -1. Beides war kein
|
||
Fehler der Karte, sondern ein Blick zur falschen Zeit. */
|
||
const k2 = [...document.querySelectorAll('.spendenkarte[data-da="ja"]')]
|
||
.find((x) => x.querySelector(".spendenkarte__wer")?.textContent === "Probe");
|
||
if (!k2) return null;
|
||
const r = k2.getBoundingClientRect();
|
||
const c = getComputedStyle(k2);
|
||
const betrag = k2.querySelector(".spendenkarte__betrag");
|
||
return {
|
||
schrift: Math.round(parseFloat(c.fontSize) * 100) / 100,
|
||
breit: Math.round(r.width), hoch: Math.round(r.height),
|
||
betragText: betrag?.textContent || "",
|
||
betragSchrift: Math.round(parseFloat(getComputedStyle(betrag).fontSize) * 100) / 100,
|
||
stufe: k2.querySelector(".spendenkarte__stufe")?.textContent || "",
|
||
wer: k2.querySelector(".spendenkarte__wer")?.textContent || "",
|
||
links: Math.round(r.left), oben: Math.round(r.top),
|
||
/* ALLE VIER SEITEN. Die erste Fassung hat nur rechts und unten
|
||
geprueft -- eine Karte, die links heraussteht, waere
|
||
durchgegangen. Im Stream ist links genauso abgeschnitten wie
|
||
rechts. */
|
||
imBild: r.left >= -1 && r.top >= -1
|
||
&& r.right <= window.innerWidth + 1 && r.bottom <= window.innerHeight + 1,
|
||
};
|
||
});
|
||
};
|
||
|
||
const kKlein = await messeKarte(16);
|
||
console.log(` Größe 1: Karte ${kKlein?.breit}x${kKlein?.hoch} px, Schrift ${kKlein?.schrift} px, `
|
||
+ `Betrag „${kKlein?.betragText}" bei ${kKlein?.betragSchrift} px, Stufe „${kKlein?.stufe}"`);
|
||
if (!kKlein || kKlein.wer !== "Probe") {
|
||
console.log(" ACHTUNG: Die Probe zeigt keine Karte -- oder sie ist nicht als Probe erkennbar.");
|
||
ausgang = 1;
|
||
}
|
||
await bild(host.seite, "18-karte-normal");
|
||
|
||
/* DIE PROBE DARF NUR BEIM DRUECKENDEN ERSCHEINEN. Eine Probe, die im
|
||
ganzen Saal laeuft, ist eine Spende, die es nicht gab. */
|
||
const beimZuschauer = await zuschauer.seite.evaluate(() =>
|
||
document.querySelectorAll(".spendenkarte").length);
|
||
console.log(` Karten bei der Zuschauerin waehrend der Probe: ${beimZuschauer} (soll: 0)`);
|
||
if (beimZuschauer !== 0) {
|
||
console.log(" ACHTUNG: Die Probe laeuft im ganzen Saal.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* Jetzt auf doppelte Größe -- und nachmessen. */
|
||
await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll("#stufen .stufe-zeile")][2];
|
||
k2.querySelector(".stufe-groesse").value = "2";
|
||
k2.querySelector(".stufe-groesse").dispatchEvent(new Event("input", { bubbles: true }));
|
||
k2.querySelector(".stufe-farbe").value = "#55d6a0";
|
||
k2.querySelector(".stufe-farbe").dispatchEvent(new Event("input", { bubbles: true }));
|
||
document.getElementById("stufen-sichern").click();
|
||
});
|
||
await host.seite.waitForTimeout(1400);
|
||
const kGross = await messeKarte(32);
|
||
console.log(` Größe 2: Karte ${kGross?.breit}x${kGross?.hoch} px, Schrift ${kGross?.schrift} px, `
|
||
+ `Betrag bei ${kGross?.betragSchrift} px, links ${kGross?.links} / oben ${kGross?.oben}, `
|
||
+ `ganz im Bild=${kGross?.imBild}`);
|
||
await bild(host.seite, "19-karte-doppelt");
|
||
|
||
if (!kGross || !kKlein) {
|
||
console.log(" ACHTUNG: Die Karte liess sich nicht zweimal messen.");
|
||
ausgang = 1;
|
||
} else {
|
||
/* ZWEIFACH HEISST ZWEIFACH. Wenn eine einzige Regel noch in `rem`
|
||
stünde statt in `em`, wüchse die Karte ungleichmäßig -- und das
|
||
sieht man auf einem Bildschirmfoto nicht, weil beides „größer"
|
||
aussieht. */
|
||
const faktor = kGross.betragSchrift / kKlein.betragSchrift;
|
||
console.log(` Der Betrag wuchs um das ${faktor.toFixed(2)}-fache (soll: 2,00)`);
|
||
if (Math.abs(faktor - 2) > 0.05) {
|
||
console.log(" ACHTUNG: Die Größe wirkt nicht oder nicht gleichmaessig.");
|
||
ausgang = 1;
|
||
}
|
||
if (kGross.hoch <= kKlein.hoch) {
|
||
console.log(" ACHTUNG: Die Karte wird nicht hoeher.");
|
||
ausgang = 1;
|
||
}
|
||
/* UND SIE MUSS INS BILD PASSEN. Die Obergrenze 2,5 ist daran
|
||
ausgerichtet -- eine Karte, die rechts heraussteht, ist im
|
||
Stream abgeschnitten. */
|
||
if (!kGross.imBild) {
|
||
console.log(" ACHTUNG: Die doppelt grosse Karte steht aus dem Bild heraus.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
|
||
/* ---- Und die Farbe? ---- */
|
||
const farbe = await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll('.spendenkarte[data-da="ja"]')]
|
||
.find((x) => x.querySelector(".spendenkarte__wer")?.textContent === "Probe");
|
||
return k2 ? getComputedStyle(k2).getPropertyValue("--sp-farbe").trim() : "";
|
||
});
|
||
console.log(` Farbe der Karte: „${farbe}" (gesetzt war #55d6a0)`);
|
||
if (farbe.toLowerCase() !== "#55d6a0") {
|
||
console.log(" ACHTUNG: Die Farbe kommt nicht an.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* Wieder auf einen vernuenftigen Wert -- die naechste Messung soll
|
||
nicht mit einer Karte in doppelter Groesse anfangen. */
|
||
await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll("#stufen .stufe-zeile")][2];
|
||
k2.querySelector(".stufe-groesse").value = "1.45";
|
||
k2.querySelector(".stufe-groesse").dispatchEvent(new Event("input", { bubbles: true }));
|
||
k2.querySelector(".stufe-farbe").value = "#e0a13a";
|
||
k2.querySelector(".stufe-farbe").dispatchEvent(new Event("input", { bubbles: true }));
|
||
document.getElementById("stufen-sichern").click();
|
||
});
|
||
await host.seite.waitForTimeout(1000);
|
||
|
||
/* ---- Das Video: Tempo, Anordnung, Umschalter --------------------
|
||
|
||
Filipe: „auch bei den videos sachen wie pause geschwindigkeit ...
|
||
nur mein bild, nur das video ... auch gerne so dass ich vielleicht
|
||
noch ein zweites nebenbei vorbereiten kann."
|
||
|
||
GEMESSEN WIRD DIE WIRKUNG, NICHT DIE EINSTELLUNG. Dass in der
|
||
Datenbank 1.5 steht, sagt nichts darüber, ob das Video schneller
|
||
läuft -- YouTube ignoriert eine unmögliche Geschwindigkeit
|
||
schweigend. Also wird die Uhr zweimal abgelesen und nachgerechnet. */
|
||
console.log("");
|
||
console.log(" --- Video: Tempo, Anordnung, Umschalter ---");
|
||
|
||
/* Die Regie zeigt „1:23 / 4:56". Das ist die einzige Stelle, an der
|
||
der Spielstand als sichtbarer Text steht -- und damit die einzige,
|
||
an der man von aussen nachrechnen kann. */
|
||
const uhrLesen = (seite) => seite.evaluate(() => {
|
||
const t = document.getElementById("v-zeit")?.textContent || "";
|
||
const [links] = t.split("/");
|
||
const [m, sek] = String(links).trim().split(":").map(Number);
|
||
return Number.isFinite(m) && Number.isFinite(sek) ? m * 60 + sek : null;
|
||
});
|
||
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="sendung"]').catch(() => {});
|
||
await host.seite.waitForTimeout(600);
|
||
|
||
const tempoKnoepfe = await host.seite.evaluate(() =>
|
||
[...document.querySelectorAll("#tempo .tempo__knopf")].map((k2) => ({
|
||
wort: k2.textContent.trim(),
|
||
hoch: Math.round(k2.getBoundingClientRect().height),
|
||
breit: Math.round(k2.getBoundingClientRect().width),
|
||
})));
|
||
console.log(` ${tempoKnoepfe.length} Tempo-Knoepfe: `
|
||
+ tempoKnoepfe.map((t) => t.wort).join(" "));
|
||
if (tempoKnoepfe.length !== 6 || tempoKnoepfe.some((t) => t.breit < 44)) {
|
||
console.log(" ACHTUNG: Die Tempo-Knoepfe fehlen oder sind zu schmal.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* NORMALES TEMPO ZUERST MESSEN -- als Bezugswert. Ohne ihn wüsste man
|
||
nicht, ob 1,5x wirklich schneller ist oder ob das Video einfach
|
||
schon lief. */
|
||
await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll("#tempo .tempo__knopf")]
|
||
.find((x) => Number(x.dataset.tempo) === 1);
|
||
if (k2) k2.click();
|
||
});
|
||
await host.seite.waitForTimeout(1500);
|
||
const a1 = await uhrLesen(host.seite);
|
||
await host.seite.waitForTimeout(6000);
|
||
const a2 = await uhrLesen(host.seite);
|
||
|
||
await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll("#tempo .tempo__knopf")]
|
||
.find((x) => Number(x.dataset.tempo) === 2);
|
||
if (k2) k2.click();
|
||
});
|
||
await host.seite.waitForTimeout(1800);
|
||
const b1 = await uhrLesen(host.seite);
|
||
await host.seite.waitForTimeout(6000);
|
||
const b2 = await uhrLesen(host.seite);
|
||
|
||
const normal = (a1 !== null && a2 !== null) ? (a2 - a1) / 6 : null;
|
||
const schnell = (b1 !== null && b2 !== null) ? (b2 - b1) / 6 : null;
|
||
console.log(` 1x: ${normal === null ? "?" : normal.toFixed(2)} Sekunden je Sekunde · `
|
||
+ `2x: ${schnell === null ? "?" : schnell.toFixed(2)}`);
|
||
if (normal === null || schnell === null || schnell < normal * 1.5) {
|
||
console.log(" ACHTUNG: Die Geschwindigkeit wirkt nicht -- das Video laeuft gleich schnell.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* UND DER ZUSCHAUER? Er muss es SEHEN -- sonst haelt er seine Leitung
|
||
fuer kaputt. */
|
||
await zuschauer.seite.waitForTimeout(1200);
|
||
const schild = await zuschauer.seite.evaluate(() => {
|
||
const s2 = document.getElementById("leinwand-tempo");
|
||
return {
|
||
versteckt: !!s2?.hidden, wort: s2?.textContent || "",
|
||
amKino: document.getElementById("teil-live")?.dataset.tempo,
|
||
};
|
||
});
|
||
console.log(` Die Zuschauerin sieht: „${schild.wort}" `
|
||
+ `(versteckt=${schild.versteckt}, am Kino ${schild.amKino})`);
|
||
if (schild.versteckt || !schild.wort.includes("2") || schild.amKino !== "2") {
|
||
console.log(" ACHTUNG: Der Zuschauerin wird die Geschwindigkeit nicht gezeigt.");
|
||
ausgang = 1;
|
||
}
|
||
await host.seite.evaluate(() => {
|
||
const k2 = [...document.querySelectorAll("#tempo .tempo__knopf")]
|
||
.find((x) => Number(x.dataset.tempo) === 1);
|
||
if (k2) k2.click();
|
||
});
|
||
await zuschauer.seite.waitForTimeout(1200);
|
||
const wieder = await zuschauer.seite.evaluate(() =>
|
||
!!document.getElementById("leinwand-tempo")?.hidden);
|
||
console.log(` Zurueck auf 1x: Schild wieder weg = ${wieder}`);
|
||
if (!wieder) {
|
||
console.log(" ACHTUNG: Das Schild bleibt stehen, obwohl es normal laeuft.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* ---- Die fuenf Anordnungen, beim Zuschauer gemessen ---- */
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="bild"]').catch(() => {});
|
||
await host.seite.waitForTimeout(500);
|
||
const anordnungen = {};
|
||
for (const [name, was] of [
|
||
["kino", "Video groß"], ["gleich", "Gleich groß"], ["kamera", "Kameras groß"],
|
||
["nur_video", "Nur Video"], ["nur_kamera", "Nur Kamera"],
|
||
]) {
|
||
await host.seite.click(`#layout-reihe .layout-knopf[data-layout="${name}"]`).catch(() => {});
|
||
await host.seite.waitForTimeout(1100);
|
||
anordnungen[name] = await zuschauer.seite.evaluate(() => {
|
||
const sicht = (w) => {
|
||
const e = document.querySelector(w);
|
||
if (!e) return "(fehlt)";
|
||
const c = getComputedStyle(e);
|
||
const r = e.getBoundingClientRect();
|
||
return c.display === "none" ? "weg" : `${Math.round(r.width)}x${Math.round(r.height)}`;
|
||
};
|
||
/* LAEUFT ETWAS UEBER? Die Leinwand hat `overflow: hidden` --
|
||
ein Kamerafenster, das daneben liegt, ist damit unsichtbar
|
||
und nicht etwa abgeschnitten sichtbar. Genau so verschwand am
|
||
28.09. bei „nur Kamera“ der zweite Gast komplett: Behaelter
|
||
2175 px breit in einer 1072 px breiten Leinwand. Auf dem Bild
|
||
sah es aus wie eine Anordnung fuer einen -- und niemand haette
|
||
gefragt, wo der zweite geblieben ist. */
|
||
const lein = document.querySelector(".leinwand")?.getBoundingClientRect();
|
||
const kk = document.querySelector(".kameras")?.getBoundingClientRect();
|
||
const fenster = [...document.querySelectorAll(".kamera")];
|
||
return {
|
||
layout: document.getElementById("teil-live")?.dataset.layout,
|
||
video: sicht(".leinwand__video"), kameras: sicht(".kameras"),
|
||
kamera: sicht(".kamera"),
|
||
ueber: (lein && kk) ? Math.round(Math.max(0, kk.width - lein.width)) : 0,
|
||
fenster: fenster.length,
|
||
/* Und liegt auch jedes einzelne innerhalb? */
|
||
drin: !lein ? 0 : fenster.filter((f) => {
|
||
const r = f.getBoundingClientRect();
|
||
return r.left >= lein.left - 1 && r.right <= lein.right + 1
|
||
&& r.top >= lein.top - 1 && r.bottom <= lein.bottom + 1;
|
||
}).length,
|
||
};
|
||
});
|
||
const m = anordnungen[name];
|
||
console.log(` ${was.padEnd(12)} (${name}): Video ${m.video}, `
|
||
+ `Kameras ${m.kameras}, eine ${m.kamera}, ${m.drin}/${m.fenster} im Bild`
|
||
+ (m.ueber ? `, UEBER ${m.ueber} px` : ""));
|
||
if (m.ueber > 0 || (m.fenster > 0 && m.drin !== m.fenster && name !== "nur_video")) {
|
||
console.log(` ACHTUNG: Bei „${was}“ liegt ein Kamerafenster ausserhalb der Leinwand.`);
|
||
ausgang = 1;
|
||
}
|
||
await bild(zuschauer.seite, `15-anordnung-${name}`);
|
||
}
|
||
/* DAS WEGLASSEN MUSS WIRKLICH WEGLASSEN. `display: none` und nicht
|
||
nur klein -- ein unsichtbarer YouTube-Rahmen spielt weiter und
|
||
haelt den Ton. */
|
||
if (anordnungen.nur_video.kameras !== "weg") {
|
||
console.log(" ACHTUNG: Bei „nur Video“ stehen die Kameras noch da.");
|
||
ausgang = 1;
|
||
}
|
||
if (anordnungen.nur_kamera.video !== "weg") {
|
||
console.log(" ACHTUNG: Bei „nur Kamera“ steht das Video noch da.");
|
||
ausgang = 1;
|
||
}
|
||
/* UND DIE KAMERAS MUESSEN DABEI GROSS WERDEN. Ein Weglassen, nach dem
|
||
zwei Briefmarken in einer schwarzen Flaeche stehen, ist keine
|
||
Anordnung, sondern ein Fehler. */
|
||
const klein = Number(String(anordnungen.kino.kamera).split("x")[0]) || 0;
|
||
const gross = Number(String(anordnungen.nur_kamera.kamera).split("x")[0]) || 0;
|
||
console.log(` Kamerabreite: kino ${klein} px -> nur_kamera ${gross} px`);
|
||
if (gross <= klein * 1.5) {
|
||
console.log(" ACHTUNG: Bei „nur Kamera“ wird die Kamera nicht groesser.");
|
||
ausgang = 1;
|
||
}
|
||
await host.seite.click('#layout-reihe .layout-knopf[data-layout="kino"]').catch(() => {});
|
||
await host.seite.waitForTimeout(800);
|
||
|
||
/* ---- Der Umschalter zwischen zwei Videos ---- */
|
||
await host.seite.click('#reiter .reiter__knopf[data-tafel="sendung"]').catch(() => {});
|
||
await host.seite.waitForTimeout(500);
|
||
const vorherVerborgen = await host.seite.evaluate(() =>
|
||
!!document.getElementById("v-tauschen")?.hidden);
|
||
console.log(` Ohne zweites Video ist der Umschalter verborgen: ${vorherVerborgen}`);
|
||
if (!vorherVerborgen) {
|
||
console.log(" ACHTUNG: Ein Umschalter ohne Gegenstueck ist eine Falle.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* KEIN STILLES VERSCHLUCKEN. `.catch(() => {})` hat hier den
|
||
Grund weggeworfen, warum das zweite Video nie ankam -- und die
|
||
naechste Zeile meldete dann „Der Umschalter kommt nicht", als
|
||
waere das Programm kaputt. Ein Messfehler, der wie ein
|
||
Programmfehler aussieht, kostet mehr Zeit als gar keine Messung. */
|
||
try {
|
||
/* ERST HINSCHAUEN, DANN DRUECKEN. Die Regie rollt senkrecht
|
||
(346 Pixel Platz, 1130 Pixel Inhalt); „Bereitlegen" steht am
|
||
unteren Ende, gemessen bei y=1430 in einem 900 Pixel hohen
|
||
Fenster. Ein Mensch scrollt dorthin, bevor er drueckt -- die
|
||
Messung muss dasselbe tun, sonst misst sie etwas, das niemand
|
||
erlebt. */
|
||
await host.seite.locator("#p-zweit").scrollIntoViewIfNeeded();
|
||
await host.seite.fill("#p-zweit", "https://youtu.be/9bZkp7q19f0");
|
||
await host.seite.locator("#p-zweit-sichern").scrollIntoViewIfNeeded();
|
||
await host.seite.click("#p-zweit-sichern");
|
||
} catch (f) {
|
||
console.log(` ACHTUNG: das zweite Video liess sich nicht eintragen -- ${f.message.split(String.fromCharCode(10))[0]}`);
|
||
ausgang = 1;
|
||
}
|
||
await host.seite.waitForTimeout(2600);
|
||
const umschalter = await host.seite.evaluate(() => {
|
||
const k2 = document.getElementById("v-tauschen");
|
||
const r = k2.getBoundingClientRect();
|
||
return {
|
||
versteckt: !!k2.hidden, wort: k2.textContent.replace(/\s+/g, " ").trim(),
|
||
breit: Math.round(r.width), hoch: Math.round(r.height),
|
||
laeuftJetzt: document.getElementById("spur-titel")?.textContent || "",
|
||
};
|
||
});
|
||
console.log(` Umschalter: „${umschalter.wort}" (${umschalter.breit}x${umschalter.hoch} px)`);
|
||
console.log(` Es laeuft: „${umschalter.laeuftJetzt}"`);
|
||
if (umschalter.versteckt || umschalter.hoch < 40) {
|
||
console.log(" ACHTUNG: Der Umschalter kommt nicht oder ist zu klein.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* EINMAL HIN. Die Stelle muss sich merken -- ohne das waere „hin und
|
||
her" ein Neustart von vorn, und dann schaltet niemand zweimal. */
|
||
const stelleVorher = await uhrLesen(host.seite);
|
||
await host.seite.click("#v-tauschen");
|
||
await host.seite.waitForTimeout(3000);
|
||
const nachHin = await host.seite.evaluate(() => ({
|
||
titel: document.getElementById("spur-titel")?.textContent || "",
|
||
zurueckZu: document.getElementById("v-tausch-titel")?.textContent || "",
|
||
zeit: document.getElementById("v-zeit")?.textContent || "",
|
||
}));
|
||
console.log(` Nach dem Wechseln laeuft: „${nachHin.titel}", zurueck zu „${nachHin.zurueckZu}"`);
|
||
|
||
/* UND ZURUECK. */
|
||
await host.seite.click("#v-tauschen");
|
||
await host.seite.waitForTimeout(3000);
|
||
const nachZurueck = await host.seite.evaluate(() => ({
|
||
titel: document.getElementById("spur-titel")?.textContent || "",
|
||
zeit: document.getElementById("v-zeit")?.textContent || "",
|
||
}));
|
||
const stelleNachher = await uhrLesen(host.seite);
|
||
console.log(` Zurueck bei: „${nachZurueck.titel}", Uhr ${nachZurueck.zeit} `
|
||
+ `(vorher war ${stelleVorher} s, jetzt ${stelleNachher} s)`);
|
||
if (nachHin.titel === nachZurueck.titel) {
|
||
console.log(" ACHTUNG: Der Umschalter wechselt nicht.");
|
||
ausgang = 1;
|
||
}
|
||
/* Die gemerkte Stelle: etwa dort weiter, wo es aufgehoert hat --
|
||
sechs Sekunden Spiel fuer das Laden des Videos. */
|
||
if (stelleVorher === null || stelleNachher === null
|
||
|| Math.abs(stelleNachher - stelleVorher) > 8) {
|
||
console.log(` ACHTUNG: Die gemerkte Stelle ging verloren `
|
||
+ `(${stelleVorher} -> ${stelleNachher}).`);
|
||
ausgang = 1;
|
||
}
|
||
await bild(host.seite, "16-umschalter");
|
||
await host.seite.fill("#p-zweit", "").catch(() => {});
|
||
await host.seite.click("#p-zweit-sichern").catch(() => {});
|
||
await host.seite.waitForTimeout(900);
|
||
|
||
console.log("");
|
||
console.log(" --- Rollen und Moderation ---");
|
||
const modi = await alsWer("modi", 1440, 900);
|
||
await modi.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "domcontentloaded" });
|
||
await modi.seite.waitForTimeout(2500);
|
||
|
||
const abz = await modi.seite.evaluate(() => {
|
||
const liste = document.getElementById("chat-liste");
|
||
const marken = [...liste.querySelectorAll(".beitrag__rolle")].map((m) => ({
|
||
wort: m.textContent.trim(),
|
||
rolle: m.dataset.rolle,
|
||
schrift: Math.round(parseFloat(getComputedStyle(m).fontSize) * 100) / 100,
|
||
breit: Math.round(m.getBoundingClientRect().width),
|
||
}));
|
||
/* LAEUFT EINE ZEILE UEBER? Nicht die Zahl aus dem Stilblatt, die
|
||
tatsaechliche Breite gegen die tatsaechliche Liste. */
|
||
const innen = liste.getBoundingClientRect();
|
||
const ueber = [...liste.querySelectorAll(".beitrag")]
|
||
.map((b) => Math.round(b.scrollWidth - b.clientWidth))
|
||
.filter((x) => x > 0);
|
||
/* Und die Kopfzeile selbst -- dort stehen Name, Abzeichen und
|
||
Knopf nebeneinander. */
|
||
const koepfe = [...liste.querySelectorAll(".beitrag__wer")]
|
||
.map((h) => Math.round(h.scrollWidth - h.clientWidth))
|
||
.filter((x) => x > 0);
|
||
return {
|
||
marken, ueber, koepfe,
|
||
listenbreite: Math.round(innen.width),
|
||
namensknoepfe: liste.querySelectorAll(".beitrag__name").length,
|
||
beitraege: liste.children.length,
|
||
};
|
||
});
|
||
console.log(` Schiene ${abz.listenbreite} px breit, ${abz.beitraege} Beitraege, `
|
||
+ `${abz.marken.length} Abzeichen, ${abz.namensknoepfe} Namen zum Anfassen.`);
|
||
for (const m of abz.marken) {
|
||
console.log(` „${m.wort}" (${m.rolle}) ${m.schrift} px, ${m.breit} px breit`);
|
||
}
|
||
if (abz.marken.length < 3) {
|
||
console.log(" ACHTUNG: Die Abzeichen kommen im Chat nicht an.");
|
||
ausgang = 1;
|
||
}
|
||
/* 11,5 px ist die Untergrenze des Hauses (pruef-css-klassen). */
|
||
if (abz.marken.some((m) => m.schrift < 11.5)) {
|
||
console.log(" ACHTUNG: Ein Abzeichen ist kleiner als 11,5 px.");
|
||
ausgang = 1;
|
||
}
|
||
if (abz.ueber.length || abz.koepfe.length) {
|
||
console.log(` ACHTUNG: ${abz.ueber.length} Beitraege und ${abz.koepfe.length} Kopfzeilen `
|
||
+ `laufen ueber (${[...abz.ueber, ...abz.koepfe].join(", ")} px).`);
|
||
ausgang = 1;
|
||
}
|
||
/* EIN MODI MELDET NICHT -- er handelt. Der Melden-Knopf gehoert
|
||
denen, die nichts anderes tun koennen. */
|
||
const modiSicht = await modi.seite.evaluate(() =>
|
||
document.querySelectorAll("#chat-liste .beitrag__melden").length);
|
||
console.log(` Der Modi sieht ${modiSicht} Melden-Knoepfe (soll: 0) `
|
||
+ `und ${abz.namensknoepfe} Namen zum Anfassen.`);
|
||
if (modiSicht !== 0 || abz.namensknoepfe < 3) {
|
||
console.log(" ACHTUNG: Melden und Moderieren sind vertauscht.");
|
||
ausgang = 1;
|
||
}
|
||
|
||
/* DAS MENUE MUSS INS BILD PASSEN. Ein Menue, das unten oder rechts
|
||
heraussteht, ist auf dem Handy die Haelfte der Zeit nicht
|
||
bedienbar -- und man sieht es erst, wenn man es aufmacht. */
|
||
/* ==== AUF DAS MENUE WARTEN, NICHT AUF DIE UHR ===================
|
||
|
||
Hier stand `click(...).catch(() => {})` und danach 400 ms Pause.
|
||
Beide Haelften waren falsch:
|
||
|
||
Der `.catch()` verschluckte einen fehlgeschlagenen Klick still.
|
||
Die Chatliste wird bei jeder Meldung neu gezeichnet, und trifft
|
||
das genau zwischen Auswahl und Klick, ist der Knoten weg.
|
||
|
||
Und 400 ms sind eine Wette. Gemessen: Derselbe Lauf war am
|
||
28.09. dreimal gruen und beim vierten Mal rot -- geaendert
|
||
hatte sich am Programm nichts, nur an dieser Messung. Ein
|
||
Ergebnis, das ohne Codeaenderung kippt, ist ein Fehler der
|
||
Messung und kein Befund.
|
||
|
||
Jetzt wird auf das MERKMAL gewartet und bis zu dreimal neu
|
||
angesetzt -- und die Zahl der Versuche steht im Protokoll. Ein
|
||
stiller Wiederholungsversuch waere nur eine leisere Wette. */
|
||
let menueVersuche = 0;
|
||
let menueOffen = false;
|
||
let letzterGrund = "";
|
||
/* DER KLICK IST EIN UMSCHALTER -- ALSO ERST NACHSEHEN, DANN KLICKEN.
|
||
Die Schleife klickte bis zu dreimal blind. Der erste Klick
|
||
oeffnet das Menue, der zweite SCHLIESST es wieder, der dritte
|
||
oeffnet es erneut. Gemessen am 28.09.2026: Eine eingebaute Spur
|
||
zeigte „nach dem Klick Menue: true", waehrend die Messung
|
||
danebenstehend „geht nicht auf" meldete -- sie hatte es selbst
|
||
zugeklickt. Ein Wiederholungsversuch, der den Zustand aendert,
|
||
den er pruefen soll, misst am Ende sich selbst. */
|
||
while (menueVersuche < 3 && !menueOffen) {
|
||
menueVersuche += 1;
|
||
const schonOffen = await modi.seite.evaluate(() => !!document.querySelector(".modmenue"));
|
||
if (!schonOffen) {
|
||
/* IM BROWSER KLICKEN, NICHT VON AUSSEN.
|
||
Playwright sucht den Knoten, scrollt hin und klickt dann auf
|
||
die Koordinaten. Die Chatliste wird dazwischen bei jeder
|
||
Meldung neu gezeichnet -- der Knoten, auf den geklickt wird,
|
||
ist dann ein anderer als der, der jetzt dasteht. Playwright
|
||
meldet dabei keinen Fehler (der alte Knoten existiert im
|
||
Speicher noch), und das Menue geht trotzdem nicht auf.
|
||
Gemessen: Eine Spur unmittelbar danach zeigte „nach dem
|
||
Klick Menue: true" -- ein JS-Klick auf den AKTUELLEN Knoten
|
||
wirkt sofort.
|
||
Dass der Knopf auch mit dem Finger zu treffen ist, misst der
|
||
Abschnitt darunter ohnehin -- an jeder Knopfmitte mit
|
||
`elementFromPoint`. */
|
||
const ging = await modi.seite.evaluate(() => {
|
||
const ziel = [...document.querySelectorAll("#chat-liste .beitrag__name")]
|
||
.filter((e) => e.textContent.trim() === "Nachtfalter").pop();
|
||
if (!ziel) return "kein Beitrag von Nachtfalter in der Liste";
|
||
ziel.scrollIntoView({ block: "center" });
|
||
ziel.click();
|
||
return "";
|
||
});
|
||
if (ging) letzterGrund = ging;
|
||
}
|
||
/* `attached`, NICHT `visible`. Ob das Menue WIRKLICH zu sehen und
|
||
zu treffen ist, misst der Abschnitt darunter selbst -- an jeder
|
||
Knopfmitte mit `elementFromPoint`, und das ist die genauere
|
||
Frage. Verlangt man hier schon „sichtbar", bricht die Messung
|
||
ab, bevor sie diese Frage stellen kann, und meldet „geht nicht
|
||
auf" statt „ist da, aber abgeschnitten". */
|
||
menueOffen = await modi.seite.waitForSelector(".modmenue", { state: "attached", timeout: 3000 })
|
||
.then(() => true).catch(() => false);
|
||
}
|
||
if (menueVersuche > 1) {
|
||
console.log(` (Das Menue brauchte ${menueVersuche} Anlaeufe -- die Chatliste `
|
||
+ `wurde dazwischen neu gezeichnet.)`);
|
||
}
|
||
/* OEFFNEN UND MESSEN IN EINEM ZUG.
|
||
Hier stand eine Pause von 250 ms zwischen Klick und Messung. Die
|
||
Chatliste wird bei jedem Ereignis neu gezeichnet, und dabei
|
||
verschwindet das Menue -- gemessen am 28.09.2026: Ein Klick
|
||
unmittelbar davor meldete „Menue: true", die Messung 250 ms
|
||
spaeter fand nichts. Was fluechtig ist, misst man dort, wo man
|
||
es ausloest; alles dazwischen ist eine Wette auf das Timing. */
|
||
const menue = await modi.seite.evaluate(() => {
|
||
let m = document.querySelector(".modmenue");
|
||
if (!m) {
|
||
const ziel = [...document.querySelectorAll("#chat-liste .beitrag__name")]
|
||
.filter((e) => e.textContent.trim() === "Nachtfalter").pop();
|
||
if (ziel) { ziel.scrollIntoView({ block: "center" }); ziel.click(); }
|
||
m = document.querySelector(".modmenue");
|
||
}
|
||
if (!m) return null;
|
||
const r = m.getBoundingClientRect();
|
||
const knoepfe = [...m.querySelectorAll(".modmenue__knopf")].map((k2) => ({
|
||
wort: k2.textContent.trim(),
|
||
hoch: Math.round(k2.getBoundingClientRect().height),
|
||
}));
|
||
/* IST ES WIRKLICH ZU SEHEN? „Im Fenster" reicht nicht als Frage.
|
||
Beim ersten Bau lag das Menue nachweislich komplett im Fenster
|
||
(x=1099..1315 von 1440, y=652..840 von 900) und war trotzdem
|
||
abgeschnitten -- die Chatliste rollt, und ein rollender
|
||
Vorfahre beschneidet sein Kind. Gefunden hat das nicht die
|
||
Messung, sondern das Bildschirmfoto.
|
||
Deshalb jetzt der einzige Test, der die Frage beantwortet: An
|
||
die Stelle jedes Knopfes fassen und nachsehen, was dort
|
||
tatsaechlich liegt. */
|
||
const greifbar = [...m.querySelectorAll(".modmenue__knopf")].filter((k2) => {
|
||
const q = k2.getBoundingClientRect();
|
||
const oben = document.elementFromPoint(q.left + q.width / 2, q.top + q.height / 2);
|
||
return !!oben && (k2 === oben || k2.contains(oben));
|
||
}).length;
|
||
return {
|
||
links: Math.round(r.left), rechts: Math.round(r.right),
|
||
oben: Math.round(r.top), unten: Math.round(r.bottom),
|
||
breit: Math.round(r.width), knoepfe, greifbar,
|
||
fensterBreit: window.innerWidth, fensterHoch: window.innerHeight,
|
||
};
|
||
});
|
||
if (!menue) {
|
||
/* MIT GRUND. Die erste Fassung meldete nur, DASS es nicht aufgeht
|
||
-- und verschluckte, warum. Ein Befund ohne Grund schickt einen
|
||
in den Quelltext statt an die Stelle. */
|
||
const beitraege = await modi.seite.evaluate(() => {
|
||
const namen = [...document.querySelectorAll("#chat-liste .beitrag__name")];
|
||
return { n: document.querySelectorAll("#chat-liste .beitrag").length,
|
||
namen: namen.length,
|
||
/* WER STEHT DA? Ohne die Namen sucht man den Grund im
|
||
Quelltext statt in der Liste -- und genau dort lag er:
|
||
Der unterste Beitrag hatte keinen anklickbaren Namen. */
|
||
liste: namen.map((e) => e.textContent.trim()).join(", ") };
|
||
});
|
||
console.log(` Namen in der Liste: ${beitraege.liste}`);
|
||
console.log(` ACHTUNG: Das Moderationsmenue geht nicht auf `
|
||
+ `(${beitraege.n} Beitraege, ${beitraege.namen} anklickbare Namen`
|
||
+ `${letzterGrund ? ", " + letzterGrund : ""}).`);
|
||
ausgang = 1;
|
||
} else {
|
||
console.log(` Menue ${menue.breit} px breit bei x=${menue.links}..${menue.rechts} `
|
||
+ `(Fenster ${menue.fensterBreit}), y=${menue.oben}..${menue.unten}`);
|
||
for (const b of menue.knoepfe) console.log(` „${b.wort}" ${b.hoch} px hoch`);
|
||
console.log(` ${menue.greifbar} von ${menue.knoepfe.length} Knoepfen sind anfassbar`);
|
||
const raus = menue.rechts > menue.fensterBreit || menue.links < 0
|
||
|| menue.unten > menue.fensterHoch;
|
||
if (raus) {
|
||
console.log(" ACHTUNG: Das Menue steht aus dem Bild heraus.");
|
||
ausgang = 1;
|
||
}
|
||
if (menue.greifbar !== menue.knoepfe.length) {
|
||
console.log(" ACHTUNG: Ein Knopf des Menues ist verdeckt oder abgeschnitten.");
|
||
ausgang = 1;
|
||
}
|
||
if (menue.knoepfe.length < 4 || menue.knoepfe.some((b) => b.hoch < 44)) {
|
||
console.log(" ACHTUNG: Ein Knopf im Menue ist kleiner als 44 px.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
await bild(modi.seite, "12-moderationsmenue");
|
||
|
||
/* STUMM SCHALTEN -- und beim Betroffenen nachsehen.
|
||
Der Chat steht dabei auf „offen": genau der Fall, in dem die
|
||
Auskunft vorher falsch war („Der Chat ist gerade zu"). */
|
||
/* AUFMACHEN UND DRUECKEN IN EINEM ZUG -- aus demselben Grund wie
|
||
oben: Zwischen dem Aufmachen und diesem Klick wird die Chatliste
|
||
neu gezeichnet, und dabei verschwindet das Menue. Ein Klick von
|
||
aussen trifft dann nichts, meldet aber auch nichts. */
|
||
const stummGing = await modi.seite.evaluate(() => {
|
||
if (!document.querySelector(".modmenue")) {
|
||
const ziel = [...document.querySelectorAll("#chat-liste .beitrag__name")]
|
||
.filter((e) => e.textContent.trim() === "Nachtfalter").pop();
|
||
if (ziel) { ziel.scrollIntoView({ block: "center" }); ziel.click(); }
|
||
}
|
||
const k2 = [...document.querySelectorAll(".modmenue__knopf")]
|
||
.find((x) => x.textContent.includes("stumm"));
|
||
if (!k2) return "kein Knopf „stumm“ im Menue";
|
||
k2.click();
|
||
return "";
|
||
});
|
||
if (stummGing) {
|
||
console.log(` ACHTUNG: ${stummGing}`);
|
||
ausgang = 1;
|
||
}
|
||
await modi.seite.waitForTimeout(1500);
|
||
|
||
const beimGast = await zuschauer.seite.evaluate(() => {
|
||
const f = document.getElementById("chat-feld");
|
||
return { gesperrt: !!f?.disabled, grund: f?.placeholder || "" };
|
||
});
|
||
console.log(` Die Stummgeschaltete sieht: gesperrt=${beimGast.gesperrt}, `
|
||
+ `Grund „${beimGast.grund}"`);
|
||
const serverSagt = await roh("/workspace/api/reaktion/chat", {
|
||
method: "POST", keks: k.gast, rumpf: { text: "und jetzt?" },
|
||
});
|
||
console.log(` ... und der Server: ${serverSagt.code}/${serverSagt.j?.fehler}`);
|
||
if (!beimGast.gesperrt || !beimGast.grund.includes("stumm")
|
||
|| serverSagt.j?.fehler !== "stumm_geschaltet") {
|
||
console.log(" ACHTUNG: Die Stummschaltung kommt nicht oder mit falschem Grund an.");
|
||
ausgang = 1;
|
||
}
|
||
await roh(`/workspace/api/reaktion/massnahme/${IDS.gast}`, { method: "DELETE", keks: k.admin });
|
||
await modi.seite.waitForTimeout(900);
|
||
|
||
/* MELDEN -- der Weg fuer die, die selbst nichts tun koennen. */
|
||
const letzter = await roh("/workspace/api/reaktion/chat", { keks: k.admin });
|
||
const zuMelden = (letzter.j?.beitraege || []).filter((b) => b.person_id === IDS.gast2).pop();
|
||
if (zuMelden) {
|
||
await roh(`/workspace/api/reaktion/chat/${zuMelden.id}/melden`, {
|
||
method: "POST", keks: k.gast, rumpf: { grund: "unfreundlich" },
|
||
});
|
||
}
|
||
await modi.seite.waitForTimeout(1800);
|
||
const chip = await modi.seite.evaluate(() => {
|
||
const k2 = document.getElementById("chat-meldungen");
|
||
if (!k2) return null;
|
||
const r = k2.getBoundingClientRect();
|
||
return { versteckt: !!k2.hidden, wort: k2.textContent.trim(), hoch: Math.round(r.height) };
|
||
});
|
||
console.log(` Meldungs-Zeichen: „${chip?.wort}" (versteckt=${chip?.versteckt}, `
|
||
+ `${chip?.hoch} px hoch)`);
|
||
if (!chip || chip.versteckt || !/Meldung/.test(chip.wort)) {
|
||
console.log(" ACHTUNG: Die Meldung erreicht die Moderation nicht.");
|
||
ausgang = 1;
|
||
}
|
||
await modi.seite.click("#chat-meldungen").catch(() => {});
|
||
await modi.seite.waitForTimeout(400);
|
||
const liste = await modi.seite.evaluate(() => {
|
||
const m = document.querySelector(".modmenue--liste");
|
||
if (!m) return null;
|
||
const r = m.getBoundingClientRect();
|
||
const knoepfe = [...m.querySelectorAll(".modmenue__knopf")];
|
||
const greifbar = knoepfe.filter((k2) => {
|
||
const q = k2.getBoundingClientRect();
|
||
const oben = document.elementFromPoint(q.left + q.width / 2, q.top + q.height / 2);
|
||
return !!oben && (k2 === oben || k2.contains(oben));
|
||
}).length;
|
||
return {
|
||
zeilen: m.querySelectorAll(".modmeldung").length,
|
||
text: m.querySelector(".modmeldung__text")?.textContent || "",
|
||
knoepfe: knoepfe.length, greifbar,
|
||
rechts: Math.round(r.right), unten: Math.round(r.bottom),
|
||
fensterBreit: window.innerWidth, fensterHoch: window.innerHeight,
|
||
};
|
||
});
|
||
if (!liste || liste.zeilen < 1) {
|
||
console.log(" ACHTUNG: Die Meldungsliste geht nicht auf.");
|
||
ausgang = 1;
|
||
} else {
|
||
console.log(` ${liste.zeilen} Meldung(en), erste: „${liste.text}" -- `
|
||
+ `${liste.greifbar}/${liste.knoepfe} Knoepfe anfassbar`);
|
||
if (liste.rechts > liste.fensterBreit || liste.unten > liste.fensterHoch) {
|
||
console.log(" ACHTUNG: Die Meldungsliste steht aus dem Bild heraus.");
|
||
ausgang = 1;
|
||
}
|
||
if (liste.greifbar !== liste.knoepfe) {
|
||
console.log(" ACHTUNG: Ein Knopf der Meldungsliste ist verdeckt.");
|
||
ausgang = 1;
|
||
}
|
||
}
|
||
await bild(modi.seite, "13-meldungen");
|
||
if (modi.konsole.length) console.log(" KONSOLE:", modi.konsole.slice(0, 4).join(" | "));
|
||
|
||
/* AUS DER SENDUNG -- der haerteste Fall. Wer rausgenommen wird, muss
|
||
es ERFAHREN; vorher lief bei ihm das Video weiter, und nur die
|
||
Anwesenheitsmeldung schlug still fehl. */
|
||
await roh("/workspace/api/reaktion/massnahme", {
|
||
method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast, art: "raus" },
|
||
});
|
||
await zuschauer.seite.waitForTimeout(2500);
|
||
const rausgesetzt = await zuschauer.seite.evaluate(() => {
|
||
const k2 = document.querySelector(".raus");
|
||
return {
|
||
da: !!k2, markiert: document.body.dataset.raus === "ja",
|
||
titel: k2?.querySelector(".raus__titel")?.textContent || "",
|
||
deckt: k2 ? Math.round(k2.getBoundingClientRect().height) : 0,
|
||
fensterHoch: window.innerHeight,
|
||
};
|
||
});
|
||
console.log(` Die Rausgenommene sieht: „${rausgesetzt.titel}" `
|
||
+ `(${rausgesetzt.deckt} von ${rausgesetzt.fensterHoch} px)`);
|
||
if (!rausgesetzt.da || !rausgesetzt.markiert
|
||
|| rausgesetzt.deckt < rausgesetzt.fensterHoch - 2) {
|
||
console.log(" ACHTUNG: Wer rausgenommen wird, merkt es nicht.");
|
||
ausgang = 1;
|
||
}
|
||
await bild(zuschauer.seite, "14-aus-der-sendung");
|
||
await roh(`/workspace/api/reaktion/massnahme/${IDS.gast}`, { method: "DELETE", keks: k.admin });
|
||
|
||
/* =====================================================================
|
||
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" });
|
||
await zuschauer.seite.waitForFunction(
|
||
() => !document.querySelector(".raus__titel")
|
||
&& !!document.getElementById("leinwand"),
|
||
null, { timeout: 15000 });
|
||
await zuschauer.seite.waitForTimeout(600);
|
||
|
||
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);
|
||
|
||
/* ---- 1. Das Laufband ---- */
|
||
const an = await setz({ art: "laufband", tempo: 12 });
|
||
console.log(` Band einschalten: ${an.code}`);
|
||
await host.seite.waitForTimeout(700);
|
||
await zuschauer.seite.waitForTimeout(900);
|
||
|
||
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 zuschauer.seite.waitForTimeout(800);
|
||
|
||
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 zuschauer.seite.waitForTimeout(900);
|
||
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 zuschauer.seite.waitForTimeout(900);
|
||
await bild(zuschauer.seite, "24-werbung");
|
||
|
||
/* ---- 6. Wieder aus ---- */
|
||
const aus = await setz({ art: "aus", ecke_dogi: "aus", ecke_hase: "aus" });
|
||
await zuschauer.seite.waitForTimeout(700);
|
||
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;
|
||
}
|
||
}
|
||
|
||
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);
|