Filipe, 28.09.2026:
"der text der dazu erscheint sollen die leute selber schreiben
koennen. soll auf nicht zu viel aber auch nicht zu wenig
schreiben koennen. aber es sollen personalisierte texte sein von
den leuten selbst."
"mann soll nie die summe sehen sondern die dogen auch mit dem
symbol ... es soll auch nie geld da stehen sondern Dogen."
"ich will dass die den leuten auch als punkte hinzugefuegt werden."
DER TEXT KOMMT VON DEM, DER GIBT
Bisher konnte ihn nur die Leitung tippen; beim Melden schickte der
Zuschauer gar nichts mit. Jetzt stehen nach dem Tippen auf einen
Betrag zwei Felder da: Name auf der Karte (mit dem eigenen Namen
vorausgefuellt) und der eigene Text, bis 140 Zeichen.
140 ist gemessen, nicht geraten: Die Karte steht je nach Stufe sechs
bis elf Sekunden. Bei 140 Zeichen sind das zwei bis drei Zeilen -- die
fasst man im Vorbeischauen. Bei 200 wird die Schrift auf einer kleinen
Karte so eng, dass niemand mehr hinsieht, und ein Gruss, den keiner
liest, ist schlechter als ein kurzer. Der Zaehler erscheint erst ab
30 uebrigen Zeichen; einer, der von Anfang an mitlaeuft, macht aus
einem Gruss eine Aufgabe.
UND EINE SCHRANKE DAVOR. Der Text kommt von einem Fremden und steht
gleich im Livestream. Er laeuft ohnehin durch die Bestaetigung -- neu
ist, dass die Leitung ihn dort AENDERN kann. Ohne das bliebe nur "ganz
ablehnen", und dann faellt wegen eines Wortes eine echte Spende unter
den Tisch.
NIE WIEDER GELD AUF DEM BILDSCHIRM
Ein Doge sind zehn Euro (Filipes Angabe "1 euro sind 0,10 dogen",
rueckgefragt und bestaetigt -- zwischen den beiden Lesarten liegt der
Faktor 100). Gerechnet wird in Tausendsteln, nie in Kommazahlen.
Das Entscheidende ist nicht die Beschriftung: DER BROWSER BEKOMMT
KEINEN EURO-BETRAG MEHR, auch nicht verborgen im JSON. Der Kurs steht
einmal auf dem Server. Was nicht gesendet wird, kann an keiner Stelle
versehentlich erscheinen, und niemand muss daran denken. Gemessen:
38 Felder im Stand, kein Geldfeld, kein Eurozeichen, auch nicht auf
der Buehnentafel. Der einzige Ort, an dem noch ein Euro entsteht, ist
die PayPal-Adresse -- weil PayPal ihn braucht.
DIE PUNKTE: EIN BUCH, KEIN ZAEHLER
Ein Feld `dogen` an der Person waere kuerzer gewesen -- und die zweite
Antwort auf dieselbe Frage. Der Stand IST die Summe der Buchungen, und
eine Summe kann sich nicht von ihren Posten entfernen. Weil Filipe
spaeter etwas daran haengen will ("spezielle sachen ... wo mit diesen
dogpunkten zu tun hat"), steht neben jeder Zeile ein Grund und ein
Datum: Ein Zaehler, der kleiner wird, laesst keine Frage mehr
beantworten.
Dass nie doppelt gutgeschrieben wird, entscheidet ein eindeutiger
Index in der Datenbank und keine Bedingung im Programm -- "nochmal
zeigen" und ein wiederholter Strom koennen es damit gar nicht
ausloesen.
Ohne Person keine Punkte: Eine von Hand eingetragene Spende hat oft
nur einen Vornamen auf dem Handy. Daraus eine Person zu RATEN waere
schlimmer als keine Gutschrift -- deshalb waehlt die Leitung sie aus,
und tut sie es nicht, laeuft die Karte trotzdem.
DAS SYMBOL IST VORBEREITET
Filipe: "die symbole schick ich dir spaeter." Es wird im Regiepult
hochgeladen, ohne Deploy -- bis dahin steht eine gezeichnete Muenze
da. Es liegt bei den Stufenbildern, weil das der einzige Weg ist, der
Bilder OHNE Anmeldung ausliefert; sonst fehlte es ausgerechnet im
Stream.
=======================================================================
UND EINE REPARATUR AN DEM, WAS HEUTE MITTAG LIVE GING
Seit 3fedeea7 war der Buehnenmodus (reaktion.html?nur=buehne) kaputt:
Leinwand null Pixel hoch, im Stream ein schwarzes Bild. Das ist die
Fensterquelle fuer den Fall, dass Gaeste im Bild sind.
Ursache: Teil A hat die Zeilen des Saals festgenagelt (#teil-live auf
Zeile 2). Im Buehnenmodus stand aber seit jeher eine eigene Regel, die
dem Saal nur EINE Zeile gibt -- das Video landete in einer impliziten
Zeile, die sich nach ihrem Inhalt bemisst, waehrend die Leinwand ihre
Hoehe aus der Zeile nimmt. Beide warten aufeinander, heraus kommt
null.
Das ist heute der VIERTE Fall von "zwei Regeln fuer dieselbe Frage"
(Tafeln, Saalzeilen, Kamerabreite, Buehnenmodus). Die Loesung ist
jedes Mal dieselbe: nicht die zweite Regel richtig stellen, sondern
sie abschaffen.
DREI DINGE, DIE DARAN LEHRREICH SIND:
1. MEINE EIGENE PRUEFUNG WAR GRUEN UND WERTLOS. Ich hatte nach Teil A
extra geprueft: "genau eine Regel bestimmt die Zeilen des Saals".
Sie verlangte, dass die Zeile mit `.saal` BEGINNT -- und hat
`body[data-nur="buehne"] .saal { … }` deshalb nie gesehen. Jetzt
zaehlt sie jede Regel, in deren Auswahl `.saal` vorkommt, mit einer
Gegenprobe, die eine eingeschobene zweite wirklich findet.
2. EINE SEITE MIT ZWEI ANSICHTEN BRAUCHT BEIDE MESSUNGEN. Nach Teil A
liefen `pruef-buehne` (Schnittstelle) und `mess-reaktion` (normale
Ansicht). `mess-buehne` -- die einzige, die diese Ansicht
ueberhaupt oeffnet -- lief nicht.
3. EIN PYTHON-SKRIPT, DAS ERST AM ENDE SCHREIBT, MELDET "ok" FUER
AENDERUNGEN, DIE NIE ANKOMMEN. Eine fehlgeschlagene Zusicherung hat
einen ganzen Stapel verworfen, obwohl die erste Aenderung schon
bestaetigt war. Die Messung meldete daraufhin "Auf der Karte steht:
undefined" -- kein Fehler im Code, sondern eine Zeile, die nie
geschrieben wurde. Gefunden hat es die Messung, nicht das Lesen.
pruef-spenden 136/0 (vorher 95) - pruef-reaktion 321/0 -
pruef-buehne 36/0 - pruef-css-klassen ok - pruef-tippziele 11/0 -
mess-reaktion und mess-buehne ohne Beanstandung
Co-Authored-By: Claude Opus 5 <[email protected]>
1902 lines
90 KiB
JavaScript
1902 lines
90 KiB
JavaScript
/* =====================================================================
|
|
DIE REACTION (28.09.2026)
|
|
|
|
Geprueft wird der ganze Weg aus Filipes Kurznotiz:
|
|
|
|
Kachel sichtbar -> geschlossen -> Vorbereitung ->
|
|
Wartebereich/Chat -> Countdown -> LIVE ->
|
|
Reaction + Gaeste + Chat + PayPal -> Ende
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM DIESE PRUEFUNG UEBER DIE HAUSADRESSE SPRICHT
|
|
|
|
Die Community meldet sich ueber den STILLEN ZUGANG an -- ein Modi
|
|
oder ein Zuschauer tippt auf irgendeine Kachel und gibt seinen Code
|
|
ein, der Code allein entscheidet. Dieser Weg ist auf einer
|
|
Pruefadresse (127.0.0.1) absichtlich abgeschaltet: Er wuerde dort
|
|
eine messbare Zeitspur hinterlassen, und genau die soll er nicht.
|
|
|
|
Folge: Ohne Host-Kopfzeile koennte sich hier kein einziger
|
|
Zuschauer anmelden -- und eine Pruefung, die ihren Hauptdarsteller
|
|
nicht hereinbekommt, prueft nichts. `fetch` kann die Kopfzeile
|
|
nicht setzen, deshalb node:http.
|
|
|
|
Aufrufen mit: node server/pruef-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";
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-reakt-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die Reaction-Pruefung");
|
|
|
|
process.env.PORT = `${PORT}`;
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
/* DREI STATT DREISSIG. Sonst muesste die Pruefung dreissig Videos
|
|
anhaengen, um den vollen Fall zu erreichen -- und ein Grenzfall,
|
|
den niemand prueft, weil das Pruefen zu lange dauert, ist ein
|
|
ungeprueftes Verhalten. */
|
|
process.env.REAKTION_LISTE_MAX = "3";
|
|
|
|
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");
|
|
notbremse(260_000, "pruef-reaktion");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
const melde = (t) => console.log(t);
|
|
process.on("exit", () => { try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } });
|
|
|
|
const CREW = "crew.dogfather-universe.com";
|
|
const AGENTUR = "workspace.dogfather-universe.com";
|
|
|
|
/* ---------- Zugaenge anlegen -------------------------------------------- */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const nun = new Date().toISOString();
|
|
const kennSchluessel = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", kennSchluessel, nun);
|
|
|
|
/* 200 Tage alt: Der Treff hat Stufen, und wer neu ist, darf weniger.
|
|
Fuer die Reaction spielt das keine Rolle -- aber ein Zugang von
|
|
heute waere ein Sonderfall, den niemand im Blick hat. */
|
|
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", kennSchluessel).update(code).digest("hex"), ALT);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
const ROLLEN = [["admin", "Filipe"], ["hand", "VanVan"], ["linke", "Funny"],
|
|
["modi", "Miss"], ["gast", "Nachtfalter"], ["gast2", "Lenny"], ["creator", "Luna"]];
|
|
const CODES = {}; const IDS = {};
|
|
for (const [i, [rolle, name]] of ROLLEN.entries()) {
|
|
CODES[rolle] = `CODE-R${String(i).padStart(2, "0")}-000001`;
|
|
IDS[rolle] = anlegen(name, rolle === "gast2" ? "gast" : rolle, CODES[rolle]);
|
|
}
|
|
d.close();
|
|
|
|
/* ---------- Sprechen mit Hausadresse ------------------------------------ */
|
|
function roh(host, pfad, { method = "GET", keks = "", rumpf = null, origin } = {}) {
|
|
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: host,
|
|
Origin: origin === undefined ? `https://${host}` : origin,
|
|
...(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, host) {
|
|
const a = await roh(host, "/workspace/api/anmelden", {
|
|
method: "POST", rumpf: { rolle: rolle === "gast2" ? "gast" : rolle,
|
|
code: CODES[rolle], alter_ok: true },
|
|
});
|
|
const keks = [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
|
|
return a.code === 200 && keks ? keks : null;
|
|
}
|
|
|
|
const k = {};
|
|
for (const [rolle] of ROLLEN) k[rolle] = await anmelden(rolle, CREW);
|
|
|
|
/* =======================================================================
|
|
1. ALLE KOMMEN HEREIN — auch die Community
|
|
======================================================================= */
|
|
melde("=== 1. Wer die Reaction sieht ===");
|
|
{
|
|
const fehlend = ROLLEN.map(([r]) => r).filter((r) => r !== "creator" && !k[r]);
|
|
ok(fehlend.length === 0,
|
|
`alle Rollen des Hauses sind angemeldet (${fehlend.length ? "fehlt: " + fehlend.join(", ") : "5 von 5"})`);
|
|
|
|
for (const rolle of ["admin", "hand", "linke", "modi", "gast"]) {
|
|
const a = await roh(CREW, "/workspace/api/reaktion", { keks: k[rolle] });
|
|
ok(a.code === 200 && a.j?.stand === "zu",
|
|
`${rolle}: sieht die Lage und sie ist „zu" (${a.code}/${a.j?.stand})`);
|
|
}
|
|
const s = await roh(CREW, "/workspace/reaktion.html", { keks: k.gast });
|
|
ok(s.code === 200, `die Seite steht der Community offen (${s.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. DAS ANDERE HAUS SIEHT NICHTS
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 2. Die Agentur kennt sie nicht ===");
|
|
{
|
|
const kA = await anmelden("admin", AGENTUR);
|
|
ok(!!kA, "DogFather meldet sich auch auf der Agenturadresse an");
|
|
const a = await roh(AGENTUR, "/workspace/api/reaktion", { keks: kA });
|
|
ok(a.code === 404, `und bekommt dort 404 (${a.code})`);
|
|
const s = await roh(AGENTUR, "/workspace/reaktion.html", { keks: kA });
|
|
ok(s.code === 404, `auch die Seite gibt es dort nicht (${s.code})`);
|
|
|
|
/* Das andere Ende derselben Regel: Auf einer Pruefadresse hat
|
|
niemand ein Haus, und die Schranke darf das nicht mit dem
|
|
fremden Haus verwechseln. Genau dieser Fehler ist am 27.09.
|
|
beim Notizblock passiert. */
|
|
const kP = await anmelden("admin", "127.0.0.1");
|
|
const p = await roh("127.0.0.1", "/workspace/api/reaktion", { keks: kP });
|
|
ok(p.code === 200, `auf der Pruefadresse ist sie offen (${p.code}) -- ohne Haus wird nicht gesperrt`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. NUR DER HOST STELLT EIN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Wer die Sendung fuehrt ===");
|
|
{
|
|
/* ZWEI FUEHREN, UND NUR DIE ZWEI (Filipe, 28.09.2026):
|
|
„die rolle dogfather und vanvan sollen alles sehen und bereit
|
|
machen auch vorher. nur die zwei sollen alles sehen, vorbereiten
|
|
und einschakten koennen."
|
|
|
|
Geprueft wird deshalb in BEIDE Richtungen -- wer darf, und wer
|
|
ausdruecklich nicht. Nur die eine Haelfte zu messen hiesse: Die
|
|
Tuer laesst sich spaeter weit aufmachen, ohne dass etwas rot
|
|
wird. */
|
|
const { REAKTION_HOST_ROLLEN } = await import("./workspace-reaktion.js");
|
|
ok(REAKTION_HOST_ROLLEN.has("admin") && REAKTION_HOST_ROLLEN.has("hand")
|
|
&& REAKTION_HOST_ROLLEN.size === 2,
|
|
`genau zwei fuehren die Sendung: ${[...REAKTION_HOST_ROLLEN].join(", ")}`);
|
|
|
|
for (const rolle of ["linke", "modi", "gast"]) {
|
|
const a = await roh(CREW, "/workspace/api/reaktion", {
|
|
method: "PUT", keks: k[rolle], rumpf: { titel: "Nicht du" },
|
|
});
|
|
ok(a.code === 403, `${rolle}: darf nicht einstellen (${a.code})`);
|
|
}
|
|
|
|
/* Die rechte Hand darf ALLES, was zum Fuehren gehoert -- nicht nur
|
|
einstellen. Ein Knopf, der da ist und mit 403 antwortet, ist eine
|
|
Falle; deshalb wird jeder einzeln nachgefahren. */
|
|
const wege = [
|
|
["einstellen", "PUT", "/workspace/api/reaktion", { titel: "VanVan richtet ein" }],
|
|
["vorbereiten", "POST", "/workspace/api/reaktion/stand", { stand: "vorbereitung" }],
|
|
["Anordnung", "POST", "/workspace/api/reaktion/layout", { layout: "gleich" }],
|
|
];
|
|
for (const [was, art, pfad, rumpf] of wege) {
|
|
const a = await roh(CREW, pfad, { method: art, keks: k.hand, rumpf });
|
|
ok(a.code === 200, `die rechte Hand darf ${was} (${a.code})`);
|
|
}
|
|
await roh(CREW, "/workspace/api/reaktion/stand", { method: "POST", keks: k.hand, rumpf: { stand: "zu" } });
|
|
await roh(CREW, "/workspace/api/reaktion/layout", { method: "POST", keks: k.admin, rumpf: { layout: "kino" } });
|
|
|
|
const a = await roh(CREW, "/workspace/api/reaktion", {
|
|
method: "PUT", keks: k.admin,
|
|
rumpf: { titel: "Wir schauen den Trailer", video: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" },
|
|
});
|
|
ok(a.code === 200 && a.j?.video === "dQw4w9WgXcQ",
|
|
`DogFather auch -- und aus der Adresse wird die Kennung (${a.j?.video})`);
|
|
|
|
/* DAS REGIEPULT HAENGT AN `ich.host` UND AN NICHTS SONST. Ein
|
|
zweiter Ort, an dem der Browser dieselbe Frage noch einmal
|
|
beantwortet, waere der, der spaeter abweicht -- und dann stuenden
|
|
Knoepfe da, die mit 403 antworten. */
|
|
for (const [rolle, soll] of [["admin", true], ["hand", true],
|
|
["linke", false], ["modi", false], ["gast", false]]) {
|
|
const b = await roh(CREW, "/workspace/api/reaktion", { keks: k[rolle] });
|
|
ok(b.j?.ich?.host === soll,
|
|
`${rolle}: ${soll ? "sieht" : "sieht kein"} Regiepult (ich.host=${b.j?.ich?.host})`);
|
|
}
|
|
}
|
|
|
|
/* ---- Die Kennung kommt aus jeder Form, die man wirklich einfuegt ---- */
|
|
{
|
|
const { videoKennung } = await import("./workspace-reaktion.js");
|
|
const faelle = [
|
|
["https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=42s", "dQw4w9WgXcQ"],
|
|
["https://youtu.be/dQw4w9WgXcQ", "dQw4w9WgXcQ"],
|
|
["https://www.youtube.com/embed/dQw4w9WgXcQ", "dQw4w9WgXcQ"],
|
|
["https://www.youtube.com/shorts/dQw4w9WgXcQ", "dQw4w9WgXcQ"],
|
|
["dQw4w9WgXcQ", "dQw4w9WgXcQ"],
|
|
];
|
|
const sitzt = faelle.filter(([roh2, soll]) => videoKennung(roh2) === soll).length;
|
|
ok(sitzt === faelle.length, `alle ${faelle.length} Formen der Adresse ergeben die Kennung`);
|
|
/* Gegenprobe: Was keine Kennung ist, darf auch keine werden -- sonst
|
|
stuende vor Publikum ein schwarzes Rechteck. */
|
|
const mist = ["", "https://vimeo.com/12345", "kein-video", "https://www.youtube.com/watch?v=zu_kurz"];
|
|
ok(mist.every((m) => videoKennung(m) === ""),
|
|
"und was keine ist, wird auch keine (Gegenprobe)");
|
|
const b = await roh(CREW, "/workspace/api/reaktion", {
|
|
method: "PUT", keks: k.admin, rumpf: { video: "https://vimeo.com/12345" },
|
|
});
|
|
ok(b.code === 400 && b.j?.fehler === "video_unklar",
|
|
`eine fremde Adresse wird abgelehnt (${b.code}/${b.j?.fehler})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. DER ABLAUF
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. zu -> vorbereitung -> live -> zu ===");
|
|
{
|
|
const a = await roh(CREW, "/workspace/api/reaktion/stand", {
|
|
method: "POST", keks: k.admin, rumpf: { stand: "vorbereitung" },
|
|
});
|
|
ok(a.code === 200 && a.j?.stand === "vorbereitung", `Vorbereitung geht (${a.j?.stand})`);
|
|
|
|
const b = await roh(CREW, "/workspace/api/reaktion/stand", {
|
|
method: "POST", keks: k.admin, rumpf: { stand: "erfunden" },
|
|
});
|
|
ok(b.code === 400, `ein erfundener Stand wird abgelehnt (${b.code})`);
|
|
|
|
/* Im Wartebereich darf schon geschrieben werden -- das ist der
|
|
halbe Sinn der Vorbereitung. */
|
|
const c = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast, rumpf: { text: "Bin schon da!" },
|
|
});
|
|
ok(c.code === 201, `im Wartebereich ist der Chat offen (${c.code})`);
|
|
|
|
const e = await roh(CREW, "/workspace/api/reaktion/stand", {
|
|
method: "POST", keks: k.admin, rumpf: { stand: "live" },
|
|
});
|
|
ok(e.code === 200 && e.j?.stand === "live" && e.j?.gestartet_am,
|
|
`auf Sendung -- mit Startzeit (${e.j?.stand})`);
|
|
|
|
/* WER EINSCHALTET, IST IM BILD.
|
|
|
|
Vorher stand als Host, wer zuletzt etwas eingestellt hatte. Mit
|
|
zwei Leuten, die vorbereiten duerfen, ist das eine Falle: VanVan
|
|
richtet nachmittags ein, Filipe drueckt abends auf Sendung -- und
|
|
im Bild stuende VanVans Name, waehrend Filipe redet. */
|
|
ok(e.j?.host?.id === IDS.admin,
|
|
`wer eingeschaltet hat, fuehrt die Sendung (${e.j?.host?.name})`);
|
|
|
|
await roh(CREW, "/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "zu" } });
|
|
await roh(CREW, "/workspace/api/reaktion", {
|
|
method: "PUT", keks: k.admin, rumpf: { titel: "Wir schauen den Trailer", video: "dQw4w9WgXcQ" },
|
|
});
|
|
const f = await roh(CREW, "/workspace/api/reaktion/stand", {
|
|
method: "POST", keks: k.hand, rumpf: { stand: "live" },
|
|
});
|
|
ok(f.j?.host?.id === IDS.hand,
|
|
`drueckt die rechte Hand, fuehrt sie (${f.j?.host?.name})`);
|
|
/* Und zurueck auf DogFather, damit der Rest der Pruefung von ihm
|
|
aus weiterlaeuft. */
|
|
await roh(CREW, "/workspace/api/reaktion/stand", { method: "POST", keks: k.hand, rumpf: { stand: "zu" } });
|
|
await roh(CREW, "/workspace/api/reaktion", {
|
|
method: "PUT", keks: k.admin, rumpf: { titel: "Wir schauen den Trailer", video: "dQw4w9WgXcQ" },
|
|
});
|
|
await roh(CREW, "/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "live" } });
|
|
}
|
|
|
|
/* ---- Ohne Video kein Live ---- */
|
|
{
|
|
await roh(CREW, "/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "zu" } });
|
|
await roh(CREW, "/workspace/api/reaktion", { method: "PUT", keks: k.admin, rumpf: { titel: "Ohne", video: "" } });
|
|
const a = await roh(CREW, "/workspace/api/reaktion/stand", {
|
|
method: "POST", keks: k.admin, rumpf: { stand: "live" },
|
|
});
|
|
ok(a.code === 400 && a.j?.fehler === "kein_video",
|
|
`ohne Video laesst sich nicht senden (${a.code}/${a.j?.fehler})`);
|
|
/* und danach wieder aufbauen */
|
|
await roh(CREW, "/workspace/api/reaktion", {
|
|
method: "PUT", keks: k.admin,
|
|
rumpf: { titel: "Wir schauen den Trailer", video: "dQw4w9WgXcQ" },
|
|
});
|
|
await roh(CREW, "/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "live" } });
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. WER DABEI IST
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5. Dabei sein ===");
|
|
{
|
|
for (const rolle of ["gast", "gast2", "modi"]) {
|
|
await roh(CREW, "/workspace/api/reaktion/dabei", { method: "POST", keks: k[rolle] });
|
|
}
|
|
const a = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
|
|
ok(a.j?.dabei === 3, `drei sind dabei (${a.j?.dabei})`);
|
|
|
|
ok(Array.isArray(a.j?.zuschauer) && a.j.zuschauer.length === 3,
|
|
"DogFather sieht, WER dabei ist");
|
|
|
|
const h = await roh(CREW, "/workspace/api/reaktion", { keks: k.hand });
|
|
ok(Array.isArray(h.j?.zuschauer) && h.j.zuschauer.length === 3,
|
|
"die rechte Hand sieht es auch -- sie fuehrt mit");
|
|
|
|
const z = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast });
|
|
ok(z.j?.zuschauer === null,
|
|
"ein Zuschauer bekommt die Namensliste NICHT");
|
|
ok(z.j?.dabei === 3, `die Zahl sieht er aber (${z.j?.dabei})`);
|
|
|
|
/* „NUR DIE ZWEI SOLLEN ALLES SEHEN." Die Moderation stand hier
|
|
frueher mit drin, weil sie Beitraege wegnehmen darf -- das ist
|
|
eine Vermischung zweier Dinge, die nichts miteinander zu tun
|
|
haben. Wegnehmen duerfen sie weiter (Abschnitt 7). */
|
|
for (const rolle of ["linke", "modi"]) {
|
|
const m = await roh(CREW, "/workspace/api/reaktion", { keks: k[rolle] });
|
|
ok(m.j?.zuschauer === null,
|
|
`${rolle}: moderiert, bekommt die Namensliste aber nicht`);
|
|
}
|
|
|
|
/* Jeder Dabeisitzende hat einen Kameraplatz bekommen -- es sind
|
|
erst drei, und zwoelf waeren erlaubt. */
|
|
ok(a.j.zuschauer.every((x) => x.sicht === true),
|
|
"alle drei haben einen Kameraplatz (bei 12 Plaetzen)");
|
|
|
|
/* Abmelden: der Weg, den `sendBeacon` beim Schliessen benutzt --
|
|
POST, nicht DELETE. Beides muss gehen. */
|
|
const w1 = await roh(CREW, "/workspace/api/reaktion/dabei-weg", { method: "POST", keks: k.modi });
|
|
ok(w1.code === 200, `abmelden per POST geht (fuer sendBeacon) (${w1.code})`);
|
|
const n1 = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
|
|
ok(n1.j?.dabei === 2, `danach sind es zwei (${n1.j?.dabei})`);
|
|
await roh(CREW, "/workspace/api/reaktion/dabei", { method: "POST", keks: k.modi });
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. GAESTE
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 6. Ein bis zwei Gaeste ===");
|
|
{
|
|
const { GAESTE_MAX } = await import("./reaktion-tabellen.js");
|
|
ok(GAESTE_MAX === 2, `zwei Plaetze, wie in der Notiz (${GAESTE_MAX})`);
|
|
|
|
const a = await roh(CREW, "/workspace/api/reaktion/gast", {
|
|
method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast },
|
|
});
|
|
ok(a.code === 201 && a.j?.gaeste?.length === 1, `Gast 1 dazu (${a.code})`);
|
|
ok(a.j.gaeste[0].platz === 1, `er bekommt Platz 1 (${a.j?.gaeste?.[0]?.platz})`);
|
|
|
|
const b = await roh(CREW, "/workspace/api/reaktion/gast", {
|
|
method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast2 },
|
|
});
|
|
ok(b.code === 201 && b.j?.gaeste?.length === 2, `Gast 2 dazu (${b.code})`);
|
|
|
|
const c = await roh(CREW, "/workspace/api/reaktion/gast", {
|
|
method: "POST", keks: k.admin, rumpf: { person_id: IDS.modi },
|
|
});
|
|
ok(c.code === 409 && c.j?.fehler === "gaeste_voll",
|
|
`ein dritter passt nicht (${c.code}/${c.j?.fehler})`);
|
|
|
|
/* EIN GAST BEKOMMT DIE NAMENSLISTE -- und das ist kein Zufall:
|
|
Ohne sie wuesste er nicht, wen er anrufen soll, und sein Bild
|
|
kaeme bei keinem Zuschauer an. */
|
|
const g = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast });
|
|
ok(Array.isArray(g.j?.zuschauer),
|
|
"wer auf der Buehne steht, bekommt die Liste (sonst sieht ihn niemand)");
|
|
ok(g.j?.ich?.gast === true, "und er weiss, dass er Gast ist");
|
|
|
|
/* Stummschalten gilt fuer alle -- das steht am Server, nicht im
|
|
Browser des Hosts. */
|
|
const s = await roh(CREW, `/workspace/api/reaktion/gast/${IDS.gast}/stumm`, {
|
|
method: "PUT", keks: k.admin, rumpf: { stumm: true },
|
|
});
|
|
ok(s.code === 200 && s.j?.gaeste?.find((x) => x.id === IDS.gast)?.stumm === true,
|
|
"stummschalten wirkt");
|
|
const s2 = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast2 });
|
|
ok(s2.j?.gaeste?.find((x) => x.id === IDS.gast)?.stumm === true,
|
|
"und alle anderen sehen es auch (nicht nur der Host)");
|
|
|
|
/* Ein Gast kann sich nicht selbst holen oder entfernen. */
|
|
const f = await roh(CREW, "/workspace/api/reaktion/gast", {
|
|
method: "POST", keks: k.gast, rumpf: { person_id: IDS.modi },
|
|
});
|
|
ok(f.code === 403, `ein Gast holt niemanden dazu (${f.code})`);
|
|
const f2 = await roh(CREW, "/workspace/api/reaktion/gast", {
|
|
method: "POST", keks: k.modi, rumpf: { person_id: IDS.linke },
|
|
});
|
|
ok(f2.code === 403, `ein Modi auch nicht (${f2.code})`);
|
|
|
|
const w = await roh(CREW, `/workspace/api/reaktion/gast/${IDS.gast2}`, {
|
|
method: "DELETE", keks: k.admin,
|
|
});
|
|
ok(w.code === 200 && w.j?.gaeste?.length === 1, `entfernen geht (${w.code})`);
|
|
|
|
/* Der frei gewordene Platz wird WIEDERVERWENDET. Mit „Anzahl + 1"
|
|
bliebe Platz 1 fuer immer leer, sobald einmal jemand gegangen
|
|
ist -- und das faellt erst im dritten Stream auf. */
|
|
const neu = await roh(CREW, "/workspace/api/reaktion/gast", {
|
|
method: "POST", keks: k.admin, rumpf: { person_id: IDS.modi },
|
|
});
|
|
const plaetze = (neu.j?.gaeste || []).map((x) => x.platz).sort();
|
|
ok(plaetze.join(",") === "1,2",
|
|
`der frei gewordene Platz wird wiederverwendet (${plaetze.join(",")})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. DER LIVE-CHAT UND SEINE MODERATION
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 7. Chat und Moderation ===");
|
|
let fremderBeitrag = 0;
|
|
{
|
|
const a = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast2, rumpf: { text: "Das ist gut!" },
|
|
});
|
|
ok(a.code === 201 && a.j?.id > 0, `schreiben geht (${a.code})`);
|
|
fremderBeitrag = a.j.id;
|
|
|
|
const leer = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast2, rumpf: { text: " " },
|
|
});
|
|
ok(leer.code === 400, `ein leerer Beitrag wird abgelehnt (${leer.code})`);
|
|
|
|
const liste = await roh(CREW, "/workspace/api/reaktion/chat", { keks: k.gast });
|
|
ok(liste.code === 200 && liste.j.beitraege.some((b) => b.id === fremderBeitrag),
|
|
"und steht in der Liste");
|
|
|
|
/* Ein Zuschauer nimmt einen FREMDEN Beitrag nicht weg. */
|
|
const nein = await roh(CREW, `/workspace/api/reaktion/chat/${fremderBeitrag}`, {
|
|
method: "DELETE", keks: k.gast,
|
|
});
|
|
ok(nein.code === 403, `ein Zuschauer nimmt fremde Beitraege nicht weg (${nein.code})`);
|
|
|
|
/* Sein eigener geht. */
|
|
const eigen = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast, rumpf: { text: "ups" },
|
|
});
|
|
const weg = await roh(CREW, `/workspace/api/reaktion/chat/${eigen.j.id}`, {
|
|
method: "DELETE", keks: k.gast,
|
|
});
|
|
ok(weg.code === 200, `den eigenen schon (${weg.code})`);
|
|
|
|
/* Ein Modi moderiert. */
|
|
const mod = await roh(CREW, `/workspace/api/reaktion/chat/${fremderBeitrag}`, {
|
|
method: "DELETE", keks: k.modi,
|
|
});
|
|
ok(mod.code === 200, `ein Modi moderiert (${mod.code})`);
|
|
|
|
const nach = await roh(CREW, "/workspace/api/reaktion/chat", { keks: k.gast });
|
|
ok(!nach.j.beitraege.some((b) => b.id === fremderBeitrag),
|
|
"und der Beitrag ist aus der Liste");
|
|
|
|
/* WEGGENOMMEN UND NICHT GELOESCHT -- wer moderiert, soll spaeter
|
|
sagen koennen, was er weggenommen hat. */
|
|
const { DatabaseSync: DB2 } = await import("node:sqlite");
|
|
const d2 = new DB2(process.env.WORKSPACE_DB, { readOnly: true });
|
|
const zeile = d2.prepare("SELECT weg_am, weg_von, text FROM reaktion_chat WHERE id = ?")
|
|
.get(fremderBeitrag);
|
|
d2.close();
|
|
ok(!!zeile && !!zeile.weg_am && zeile.weg_von === IDS.modi && zeile.text === "Das ist gut!",
|
|
"er steht noch da -- mit Zeitpunkt und Namen dessen, der ihn genommen hat");
|
|
}
|
|
|
|
/* ---- Die Schreibbremse ---- */
|
|
{
|
|
const { SCHREIB_BREMSE } = await import("./reaktion-tabellen.js");
|
|
let gebremst = 0;
|
|
for (let i = 0; i < SCHREIB_BREMSE + 6; i++) {
|
|
const a = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.linke, rumpf: { text: "spam " + i },
|
|
});
|
|
if (a.code === 429) gebremst++;
|
|
}
|
|
ok(gebremst > 0, `die Schreibbremse greift (${gebremst} von ${SCHREIB_BREMSE + 6} abgewiesen)`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
8. DIE VERBINDUNGSDATEN GEHEN NUR AN LEUTE, DIE DABEI SIND
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 8. Kameras: der Hinweg ===");
|
|
{
|
|
const a = await roh(CREW, "/workspace/api/reaktion/signal", {
|
|
method: "POST", keks: k.admin, rumpf: { an: IDS.gast, daten: { typ: "angebot" } },
|
|
});
|
|
ok(a.code === 200, `an jemanden, der dabei ist, geht es (${a.code})`);
|
|
|
|
/* Die Creatorin ist nicht dabei -- und im anderen Haus. An sie darf
|
|
nichts gehen, sonst waere die Reaction ein Weg, jemanden
|
|
anzuklingeln. */
|
|
const b = await roh(CREW, "/workspace/api/reaktion/signal", {
|
|
method: "POST", keks: k.admin, rumpf: { an: IDS.creator, daten: { typ: "angebot" } },
|
|
});
|
|
ok(b.code === 409 && b.j?.fehler === "nicht_dabei",
|
|
`an jemanden, der nicht dabei ist, nicht (${b.code}/${b.j?.fehler})`);
|
|
|
|
const c = await roh(CREW, "/workspace/api/reaktion/signal", {
|
|
method: "POST", keks: k.admin, rumpf: { an: IDS.gast },
|
|
});
|
|
ok(c.code === 400, `ohne Inhalt wird abgelehnt (${c.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
9. DAS ENDE RAEUMT AUF
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 9. Beenden ===");
|
|
{
|
|
const vorher = await roh(CREW, "/workspace/api/reaktion/chat", { keks: k.admin });
|
|
ok(vorher.j.beitraege.length > 0, `vor dem Ende steht etwas im Chat (${vorher.j.beitraege.length})`);
|
|
|
|
const a = await roh(CREW, "/workspace/api/reaktion/stand", {
|
|
method: "POST", keks: k.admin, rumpf: { stand: "zu" },
|
|
});
|
|
ok(a.code === 200 && a.j?.stand === "zu", `beenden geht (${a.j?.stand})`);
|
|
ok(a.j.gaeste.length === 0, "die Gaeste sind entlassen");
|
|
ok(a.j.dabei === 0, "und niemand sitzt mehr im Saal");
|
|
|
|
const nach = await roh(CREW, "/workspace/api/reaktion/chat", { keks: k.admin });
|
|
ok(nach.j.beitraege.length === 0,
|
|
"der Live-Chat dieser Sendung ist abgeraeumt -- die naechste faengt nicht mit den Rufen der letzten an");
|
|
|
|
/* Was gelaufen ist, steht im Verlauf. */
|
|
const { DatabaseSync: DB3 } = await import("node:sqlite");
|
|
const d3 = new DB3(process.env.WORKSPACE_DB, { readOnly: true });
|
|
const v = d3.prepare("SELECT * FROM reaktion_verlauf ORDER BY id DESC LIMIT 1").get();
|
|
d3.close();
|
|
ok(!!v && v.titel === "Wir schauen den Trailer" && v.beitraege > 0,
|
|
`die Sendung steht im Verlauf (${v?.titel}, ${v?.beitraege} Beitraege)`);
|
|
|
|
/* Bei „zu" ist der Chat zu -- sonst redeten die Leute in einen
|
|
leeren Saal. */
|
|
const c = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast, rumpf: { text: "Hallo?" },
|
|
});
|
|
ok(c.code === 409 && c.j?.fehler === "saal_zu",
|
|
`und schreiben geht nicht mehr (${c.code}/${c.j?.fehler})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
10. DIE KACHEL, DIE RECHTE UND DIE INHALTSRICHTLINIE
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 10. Was um die Seite herum stimmen muss ===");
|
|
{
|
|
const { SEITEN } = await import("./rechte.js");
|
|
const erlaubt = SEITEN["/workspace/reaktion.html"] || [];
|
|
ok(erlaubt.includes("gast"),
|
|
`die Community steht in der Rechtetafel (${erlaubt.join(", ")})`);
|
|
|
|
/* Die Kachel. Gesucht wird sie am ZIEL und nicht am Namen -- Namen
|
|
haben sich in diesem Haus schon zweimal geaendert. */
|
|
const w = await import("./workspace.js");
|
|
const alle = w.bereicheFuer({ id: IDS.gast, rolle: "gast", name: "Nachtfalter" }, CREW);
|
|
const kachel = (alle || []).find((b) => b.ziel === "reaktion.html");
|
|
ok(!!kachel, "die Community hat die Reaction-Kachel");
|
|
ok(kachel?.live === true,
|
|
"sie traegt das Merkmal `live` statt einer Ton-Nummer (der Farbraum ist bei 46 voll)");
|
|
ok(!kachel?.ton, "und ausdruecklich KEINE Ton-Nummer");
|
|
|
|
/* DAS MERKMAL HEISST NICHT `data-live`. Das ist seit dem 21.09.
|
|
vergeben -- die Draussen-Kachel bekommt es, sobald Filipe auf
|
|
Twitch sendet. Haetten beide denselben Namen, waere der
|
|
Draussen-Kachel waehrend jedes Streams die Farbe abhanden
|
|
gekommen. */
|
|
const startJs = readFileSync(join(HIER, "..", "workspace", "assets", "js", "start.js"), "utf8");
|
|
ok(/dataset\.sendung = 'ja'/.test(startJs),
|
|
"die Wand setzt `data-sendung` (und nicht `data-live`, das ist vergeben)");
|
|
const startCss = readFileSync(join(HIER, "..", "workspace", "assets", "css", "start.css"), "utf8");
|
|
ok(!/\.kachel\[data-live="ja"\]\s*\{\s*--ton/.test(startCss),
|
|
"und kein Stil greift der Draussen-Kachel ihre Farbe ab");
|
|
|
|
/* DIE INHALTSRICHTLINIE. Ohne YouTube darin startet die Sendung,
|
|
alle sind da, und die Leinwand bleibt schwarz -- ohne
|
|
Fehlermeldung, nur ein Eintrag in der Browser-Konsole. Genau so
|
|
ist am 27.08.2026 der Musik-Knopf ausgefallen. */
|
|
const seite = await roh(CREW, "/workspace/reaktion.html", { keks: k.gast });
|
|
const regel = String(seite.kopf["content-security-policy"] || "");
|
|
ok(/frame-src[^;]*youtube-nocookie\.com/.test(regel),
|
|
"der YouTube-Rahmen ist erlaubt");
|
|
ok(/script-src[^;]*www\.youtube\.com/.test(regel),
|
|
"und die Einbett-API darf geladen werden");
|
|
ok(/img-src[^;]*ytimg\.com/.test(regel),
|
|
"und die Vorschaubilder auch");
|
|
}
|
|
|
|
/* =======================================================================
|
|
11. DIE WARTESCHLANGE
|
|
|
|
Vorher gab es genau EIN Videofeld. Wer zwei Sachen hintereinander
|
|
schauen wollte, tippte mitten in der Sendung eine Adresse ein --
|
|
vor Publikum, mit laufender Kamera. Ein Tippfehler war ein
|
|
schwarzes Rechteck.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 11. Die Warteschlange ===");
|
|
{
|
|
await roh(CREW, "/workspace/api/reaktion/stand", {
|
|
method: "POST", keks: k.admin, rumpf: { stand: "zu" },
|
|
});
|
|
|
|
/* ERFUNDENE KENNUNGEN, mit Absicht. Elf erlaubte Zeichen -- die
|
|
Form stimmt, das Video gibt es nicht. Damit laeuft nebenbei der
|
|
Fall mit, in dem YouTube keinen Titel herausrueckt: Der Eintrag
|
|
muss trotzdem entstehen, und im Titel muss ein LEERER Text
|
|
stehen, keine Erfindung. */
|
|
const A = "aaaaaaaaaaa", B = "bbbbbbbbbbb", C = "ccccccccccc", D = "ddddddddddd";
|
|
const haengen = (rolle, video) => roh(CREW, "/workspace/api/reaktion/liste", {
|
|
method: "POST", keks: k[rolle], rumpf: { video },
|
|
});
|
|
|
|
for (const rolle of ["linke", "modi", "gast"]) {
|
|
const a = await haengen(rolle, A);
|
|
ok(a.code === 403, `${rolle}: haengt nichts an (${a.code})`);
|
|
}
|
|
|
|
const mist = await haengen("admin", "das ist keine adresse");
|
|
ok(mist.code === 400 && mist.j?.fehler === "video_unklar",
|
|
`eine kaputte Adresse wird abgelehnt (${mist.code}/${mist.j?.fehler})`);
|
|
|
|
const eins = await roh(CREW, "/workspace/api/reaktion/liste", {
|
|
method: "POST", keks: k.admin,
|
|
rumpf: { video: `https://www.youtube.com/watch?v=${A}` },
|
|
});
|
|
ok(eins.code === 200 && eins.j?.liste?.length === 1,
|
|
`aus der Adresse wird ein Eintrag (${eins.code}, ${eins.j?.liste?.length})`);
|
|
ok(eins.j?.liste?.[0]?.video === A,
|
|
`und die Kennung stimmt (${eins.j?.liste?.[0]?.video})`);
|
|
ok(typeof eins.j?.liste?.[0]?.titel === "string",
|
|
"der Titel ist IMMER ein Text -- auch wenn YouTube nichts hergab "
|
|
+ `(hier: „${eins.j?.liste?.[0]?.titel}")`);
|
|
|
|
const doppelt = await haengen("admin", A);
|
|
ok(doppelt.code === 409 && doppelt.j?.fehler === "liste_doppelt",
|
|
`dasselbe Video nicht zweimal (${doppelt.code}/${doppelt.j?.fehler})`);
|
|
|
|
/* Die rechte Hand raeumt genauso ein -- beide bereiten vor. */
|
|
const zwei = await haengen("hand", B);
|
|
ok(zwei.code === 200 && zwei.j?.liste?.length === 2,
|
|
`die rechte Hand raeumt auch ein (${zwei.code})`);
|
|
const drei = await haengen("admin", C);
|
|
ok(drei.j?.liste?.map((z) => z.video).join(",") === [A, B, C].join(","),
|
|
`die Reihenfolge ist die des Anhaengens (${drei.j?.liste?.map((z) => z.video).join(",")})`);
|
|
|
|
const voll = await haengen("admin", D);
|
|
ok(voll.code === 409 && voll.j?.fehler === "liste_voll",
|
|
`ueber der Grenze ist Schluss (${voll.code}/${voll.j?.fehler})`);
|
|
|
|
/* ---- Schieben ---- */
|
|
const idB = drei.j.liste[1].id;
|
|
const hoch = await roh(CREW, `/workspace/api/reaktion/liste/${idB}/schieben`, {
|
|
method: "POST", keks: k.hand, rumpf: { richtung: "hoch" },
|
|
});
|
|
ok(hoch.j?.liste?.map((z) => z.video).join(",") === [B, A, C].join(","),
|
|
`hoch tauscht mit dem Nachbarn (${hoch.j?.liste?.map((z) => z.video).join(",")})`);
|
|
const nochmal = await roh(CREW, `/workspace/api/reaktion/liste/${idB}/schieben`, {
|
|
method: "POST", keks: k.admin, rumpf: { richtung: "hoch" },
|
|
});
|
|
ok(nochmal.code === 200
|
|
&& nochmal.j?.liste?.map((z) => z.video).join(",") === [B, A, C].join(","),
|
|
"ganz oben noch einmal hoch aendert nichts und ist kein Fehler");
|
|
const runter = await roh(CREW, `/workspace/api/reaktion/liste/${idB}/schieben`, {
|
|
method: "POST", keks: k.admin, rumpf: { richtung: "runter" },
|
|
});
|
|
ok(runter.j?.liste?.map((z) => z.video).join(",") === [A, B, C].join(","),
|
|
`und runter wieder zurueck (${runter.j?.liste?.map((z) => z.video).join(",")})`);
|
|
|
|
/* ---- Wer die Liste sieht ---- */
|
|
const h = await roh(CREW, "/workspace/api/reaktion", { keks: k.hand });
|
|
ok(Array.isArray(h.j?.liste) && h.j.liste.length === 3,
|
|
`die zwei sehen die ganze Liste (${h.j?.liste?.length})`);
|
|
for (const rolle of ["linke", "modi", "gast"]) {
|
|
const z = await roh(CREW, "/workspace/api/reaktion", { keks: k[rolle] });
|
|
ok(z.j?.liste === null, `${rolle}: bekommt die Warteschlange NICHT`);
|
|
ok(z.j?.naechstes === A,
|
|
`${rolle}: was als Naechstes kommt, darf er wissen (${z.j?.naechstes})`);
|
|
}
|
|
|
|
/* ---- Weiterschalten ---- */
|
|
const w = await roh(CREW, "/workspace/api/reaktion/weiter", {
|
|
method: "POST", keks: k.admin, rumpf: {},
|
|
});
|
|
ok(w.code === 200 && w.j?.video === A,
|
|
`„Naechstes" holt das oberste ins Bild (${w.j?.video})`);
|
|
ok(w.j?.liste?.length === 2 && !w.j.liste.some((z) => z.video === A),
|
|
"und nimmt es aus der Liste heraus");
|
|
ok(w.j?.laeuft === true && w.j?.sekunde === 0,
|
|
`es beginnt von vorn und laeuft (laeuft=${w.j?.laeuft}, sekunde=${w.j?.sekunde})`);
|
|
|
|
const gezielt = await roh(CREW, "/workspace/api/reaktion/weiter", {
|
|
method: "POST", keks: k.hand, rumpf: { id: w.j.liste[1].id },
|
|
});
|
|
ok(gezielt.j?.video === C,
|
|
`„Jetzt" holt genau das gewaehlte, nicht das oberste (${gezielt.j?.video})`);
|
|
|
|
const nurEins = await roh(CREW, "/workspace/api/reaktion/weiter", {
|
|
method: "POST", keks: k.gast, rumpf: {},
|
|
});
|
|
ok(nurEins.code === 403, `ein Zuschauer schaltet nicht weiter (${nurEins.code})`);
|
|
|
|
await roh(CREW, "/workspace/api/reaktion/weiter", { method: "POST", keks: k.admin, rumpf: {} });
|
|
const leer = await roh(CREW, "/workspace/api/reaktion/weiter", {
|
|
method: "POST", keks: k.admin, rumpf: {},
|
|
});
|
|
ok(leer.code === 409 && leer.j?.fehler === "liste_leer",
|
|
`eine leere Liste sagt das auch (${leer.code}/${leer.j?.fehler})`);
|
|
|
|
/* Und der Titel des laufenden Videos steht in der Lage -- auch
|
|
dann, wenn er leer ist. Ein fehlendes Feld waere `undefined`
|
|
und wuerde in der Oberflaeche zu „undefined". */
|
|
const jetzt = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast });
|
|
ok(typeof jetzt.j?.video_titel === "string",
|
|
`der Titel des laufenden Videos ist immer ein Text („${jetzt.j?.video_titel}")`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
12. DIE REGIE
|
|
|
|
Hier wird nicht das Aussehen geprueft -- das kann eine Maschine
|
|
nicht -- sondern zweierlei, das man beim Umbauen still kaputt
|
|
macht:
|
|
|
|
A) JEDES `$('id')`, das die Oberflaeche anfasst, muss es in der
|
|
Seite auch GEBEN. Ein Tippfehler ergibt sonst `null`, und der
|
|
Fehler faellt erst auf, wenn jemand den Knopf drueckt --
|
|
mitten in einer Sendung.
|
|
|
|
B) Die drei grossen Knoepfe muessen AUSSERHALB des
|
|
einklappbaren Teils liegen. Wer mitten in der Sendung
|
|
„Beenden" drueckt, drueckt es, weil etwas passiert ist. Das
|
|
darf nicht hinter einer Bewegung liegen.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 12. Die Regie ===");
|
|
{
|
|
const html = readFileSync(join(HIER, "..", "workspace", "reaktion.html"), "utf8");
|
|
const js = readFileSync(join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8");
|
|
|
|
const vorhanden = new Set([...html.matchAll(/\bid="([^"]+)"/g)].map((m) => m[1]));
|
|
/* Kommentare erst weg -- sonst zaehlt ein Beispiel im Fliesstext
|
|
als Zugriff. */
|
|
const ohneNotiz = js.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
const gesucht = [...new Set([...ohneNotiz.matchAll(/\$\('([a-z0-9-]+)'\)/g)].map((m) => m[1]))];
|
|
const fehlend = gesucht.filter((n) => !vorhanden.has(n));
|
|
ok(gesucht.length > 30 && fehlend.length === 0,
|
|
fehlend.length
|
|
? `FEHLT in der Seite: ${fehlend.join(", ")}`
|
|
: `alle ${gesucht.length} angefassten Kennungen gibt es in der Seite`);
|
|
|
|
/* GEGENPROBE: Kann diese Pruefung ueberhaupt „nein" sagen? Eine,
|
|
die immer bestaetigt, bestaetigt nichts. */
|
|
ok(!vorhanden.has("gibt-es-nicht")
|
|
&& ["p-starten", "gibt-es-nicht"].filter((n) => !vorhanden.has(n)).length === 1,
|
|
"Gegenprobe: eine erfundene Kennung wuerde auffallen");
|
|
|
|
/* Auch die Register muessen zusammenpassen. */
|
|
/* Die Reihenfolge der Merkmale im Text ist beliebig -- gesucht
|
|
wird deshalb erst die Zeile mit `role="tab"` und darin das
|
|
Merkmal. Ein Muster, das eine bestimmte Reihenfolge erwartet,
|
|
bricht beim naechsten Umformatieren. */
|
|
const reiter = [...html.matchAll(/<button[^>]*role="tab"[^>]*>/g)]
|
|
.map((m) => (m[0].match(/data-tafel="([a-z]+)"/) || [])[1])
|
|
.filter(Boolean);
|
|
const tafeln = [...html.matchAll(/class="pult-tafel" data-tafel="([a-z]+)"/g)].map((m) => m[1]);
|
|
/* GEZAEHLT, NICHT FESTGESCHRIEBEN. Hier stand „genau 5" -- und
|
|
beim sechsten Register wurde die Pruefung rot, obwohl nichts
|
|
kaputt war. Was wirklich zaehlt: zu jedem Reiter gehoert eine
|
|
Tafel, und keine Tafel steht ohne Reiter da. */
|
|
ok(reiter.length >= 5
|
|
&& reiter.length === tafeln.length
|
|
&& reiter.every((r) => tafeln.includes(r)),
|
|
`zu jedem der ${reiter.length} Register gibt es genau eine Tafel (${reiter.join(", ")})`);
|
|
const steuert = [...html.matchAll(/aria-controls="([^"]+)"/g)].map((m) => m[1]);
|
|
ok(steuert.length > 0 && steuert.every((n) => vorhanden.has(n)),
|
|
`alle ${steuert.length} Verweise mit aria-controls zeigen auf etwas Vorhandenes`);
|
|
|
|
/* B) Die grossen Knoepfe liegen ausserhalb des Klappteils. */
|
|
const woLeiste = html.indexOf('class="pult__leiste"');
|
|
const woInnen = html.indexOf('id="pult-innen"');
|
|
const woBeenden = html.indexOf('id="p-beenden"');
|
|
ok(woLeiste > 0 && woInnen > woLeiste,
|
|
"die Leiste steht vor dem einklappbaren Teil");
|
|
ok(woBeenden > woLeiste && woBeenden < woInnen,
|
|
"der Beenden-Knopf liegt in der Leiste -- also auch dann erreichbar, "
|
|
+ "wenn die Regie zugeklappt ist");
|
|
|
|
/* Und die Kuerzel, die das Programm bindet, stehen auch dabei.
|
|
Ein Kuerzel, das niemand kennt, ist keins. */
|
|
const legende = (html.match(/<p class="kuerzel">[\s\S]*?<\/p>/) || [""])[0];
|
|
for (const [taste, wort] of [["Leertaste", "Leertaste"], ["M", "M"], ["N", "N"]]) {
|
|
ok(legende.includes(`<kbd>${wort}</kbd>`), `das Kuerzel ${taste} steht in der Legende`);
|
|
}
|
|
ok(/taste === 'm' \|\| taste === 'M'/.test(js) && /taste === 'n' \|\| taste === 'N'/.test(js),
|
|
"und das Programm hoert auch darauf");
|
|
ok(/closest\('input, textarea, select/.test(js),
|
|
"wer in einem Feld tippt, tippt -- die Leertaste gehoert dann dem Text");
|
|
|
|
/* ==== KEIN KLASSENNAME ZWEIMAL ================================
|
|
|
|
reaktion.html laedt FUENF Stilvorlagen: gate.css, start.css,
|
|
reaktion.css, module.css, haus.css. Wer hier eine Klasse
|
|
erfindet, die es dort schon gibt, erbt deren Regeln -- lautlos.
|
|
|
|
Gemessen am 28.09.2026: `.tafel` gehoert seit je der
|
|
Anmeldeseite und steht dort `position: absolute`. Die
|
|
Register-Tafel der Regie trug damit NICHTS zur Hoehe des Pults
|
|
bei (114 Pixel Raster, 294 Pixel Inhalt) und malte quer ueber
|
|
Register und Kuerzel hinaus. Auf dem Bildschirmfoto sah es aus
|
|
wie ein Anzeigefehler; in Wahrheit war es ein Namensstreit.
|
|
|
|
Derselbe Fehler wie im August bei .knopf-still und .schalter --
|
|
deshalb steht er ab jetzt in einer Pruefung und nicht in einem
|
|
Kommentar. */
|
|
const roh = (datei) => readFileSync(
|
|
join(HIER, "..", "workspace", "assets", "css", datei), "utf8")
|
|
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
|
/* JEDE Klasse, die in dieser Datei ueberhaupt vorkommt. */
|
|
const klassenAus = (datei) => new Set(
|
|
[...roh(datei).matchAll(/(^|[\s,>+~(])\.(-?[a-zA-Z_][a-zA-Z0-9_-]*)/g)]
|
|
.map((m) => m[2]));
|
|
|
|
/* NUR die Klassen, die hier ein EIGENES Bauteil benennen -- also am
|
|
ANFANG eines Selektors stehen.
|
|
|
|
Der Unterschied ist der ganze Punkt. `.tafel {` war als eigenes
|
|
Bauteil gemeint und hat deshalb die Regeln der Anmeldeseite
|
|
geerbt. `body[data-nur="buehne"] .kopfleiste { display: none }`
|
|
ist das Gegenteil: eine absichtliche Bezugnahme auf eine fremde
|
|
Klasse, um sie im Buehnenmodus wegzunehmen. Wer beides gleich
|
|
behandelt, macht aus einer echten Regel eine, die staendig
|
|
grundlos anschlaegt -- und die wird weggeklickt. */
|
|
const eigeneAus = (datei) => new Set(
|
|
[...roh(datei).matchAll(/(^|[\s,>+~(}])(\.(-?[a-zA-Z_][a-zA-Z0-9_-]*)[^{};,]*)\{/g)]
|
|
.map((m) => m[3]));
|
|
|
|
const meine = eigeneAus("reaktion.css");
|
|
const streit = [];
|
|
for (const fremd of ["gate.css", "start.css", "module.css", "haus.css"]) {
|
|
for (const name of klassenAus(fremd)) if (meine.has(name)) streit.push(`${name} (${fremd})`);
|
|
}
|
|
ok(meine.size > 40 && streit.length === 0,
|
|
streit.length
|
|
? `Namensstreit: ${streit.join(", ")}`
|
|
: `alle ${meine.size} Klassen aus reaktion.css sind nur dort vergeben`);
|
|
ok(klassenAus("gate.css").has("tafel"),
|
|
"Gegenprobe: der Sucher findet die Klasse, an der es lag (.tafel in gate.css)");
|
|
|
|
/* GEGENPROBE IN BEIDE RICHTUNGEN. Eine Regel, die nur „eigenes
|
|
Bauteil" von „Bezugnahme" trennen SOLL, muss beides koennen --
|
|
sonst hat man sie nur so lange geschaerft, bis sie schweigt. */
|
|
ok(meine.has("pult-tafel"),
|
|
"Gegenprobe: ein eigenes Bauteil wird als eigenes erkannt (.pult-tafel)");
|
|
ok(!meine.has("kopfleiste") && klassenAus("reaktion.css").has("kopfleiste"),
|
|
"Gegenprobe: eine fremde Klasse, die nur weggenommen wird, gilt NICHT als eigene "
|
|
+ "(.kopfleiste im Buehnenmodus)");
|
|
|
|
/* Der Pegel wird gemessen und nicht gemalt. */
|
|
ok(/createAnalyser\(\)/.test(js) && /getByteTimeDomainData/.test(js),
|
|
"die Pegel kommen aus einer echten Messung");
|
|
ok(/schluessel === 'video'/.test(js) && /eingestellte Lautstaerke|eingestellte Lautstärke/.test(js),
|
|
"beim YouTube-Regler steht dabei, dass dort nichts gemessen werden kann");
|
|
ok(/getStats\(\)/.test(js),
|
|
"der Upload wird an der Verbindung gemessen, nicht geschaetzt");
|
|
}
|
|
|
|
/* =======================================================================
|
|
13. KAMERA UND CHAT — die zwei fehlenden Host-Steuerungen
|
|
|
|
Filipes Notiz nennt acht: Kamera, Mikrofon, Video, Gaeste,
|
|
Lautstaerke, Chat, Layout, Start/Ende. Nachgezaehlt war sechsmal
|
|
etwas da und zweimal nichts.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 13. Kamera und Chat ===");
|
|
{
|
|
/* ABSCHNITT 9 HAT DIE SENDUNG BEENDET -- also wird hier neu
|
|
aufgemacht. Sich auf den Stand eines frueheren Abschnitts zu
|
|
verlassen ist die Kopplung, die spaeter jemand aus Versehen
|
|
zerreisst; dann faellt ein Abschnitt um, an dem nichts falsch
|
|
ist. */
|
|
await roh(CREW, "/workspace/api/reaktion", {
|
|
method: "PUT", keks: k.admin,
|
|
rumpf: { titel: "Kamera und Chat", video: "dQw4w9WgXcQ" },
|
|
});
|
|
await roh(CREW, "/workspace/api/reaktion/stand", {
|
|
method: "POST", keks: k.admin, rumpf: { stand: "live" },
|
|
});
|
|
for (const rolle of ["admin", "hand", "linke", "modi", "gast", "gast2"]) {
|
|
await roh(CREW, "/workspace/api/reaktion/dabei", { method: "POST", keks: k[rolle] });
|
|
}
|
|
await roh(CREW, "/workspace/api/reaktion/gast", {
|
|
method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast2 },
|
|
});
|
|
|
|
const stand0 = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
|
|
ok(stand0.j?.stand === "live" && stand0.j?.gaeste?.length === 1,
|
|
`die Buehne steht (${stand0.j?.stand}, ${stand0.j?.gaeste?.length} Gast)`);
|
|
|
|
/* ---- Kamera ---- */
|
|
const fremd = await roh(CREW, "/workspace/api/reaktion/senden", {
|
|
method: "POST", keks: k.gast, rumpf: { kamera_aus: true },
|
|
});
|
|
ok(fremd.code === 403 && fremd.j?.fehler === "sendest_nicht",
|
|
`wer nicht sendet, schaltet nichts (${fremd.code}/${fremd.j?.fehler})`);
|
|
|
|
const aus = await roh(CREW, "/workspace/api/reaktion/senden", {
|
|
method: "POST", keks: k.admin, rumpf: { kamera_aus: true },
|
|
});
|
|
ok(aus.code === 200 && aus.j?.ich?.kamera_aus === true,
|
|
`der Host schaltet sein Bild ab (${aus.code})`);
|
|
ok(aus.j?.host?.kamera_aus === true,
|
|
"und es steht an der Sendung, damit es ALLE sehen");
|
|
|
|
/* TEILWEISE AENDERN DARF DEN ANDEREN WERT NICHT ANFASSEN. Sonst
|
|
muesste jede Seite immer beide Werte mitschicken -- und wer das
|
|
vergisst, schaltet dem anderen versehentlich das Bild wieder an. */
|
|
const nurMikro = await roh(CREW, "/workspace/api/reaktion/senden", {
|
|
method: "POST", keks: k.admin, rumpf: { mikro_aus: true },
|
|
});
|
|
ok(nurMikro.j?.ich?.mikro_aus === true && nurMikro.j?.ich?.kamera_aus === true,
|
|
"nur das Mikro zu schalten laesst die Kamera in Ruhe");
|
|
|
|
const leer = await roh(CREW, "/workspace/api/reaktion/senden", {
|
|
method: "POST", keks: k.admin, rumpf: {},
|
|
});
|
|
ok(leer.code === 400 && leer.j?.fehler === "nichts_zu_tun",
|
|
`ein Aufruf ohne Inhalt sagt das auch (${leer.code})`);
|
|
|
|
/* Ein Gast darf es fuer sich -- sonst muesste er den Host bitten. */
|
|
const gastAus = await roh(CREW, "/workspace/api/reaktion/senden", {
|
|
method: "POST", keks: k.gast2, rumpf: { kamera_aus: true },
|
|
});
|
|
ok(gastAus.code === 200,
|
|
`ein Gast schaltet sein eigenes Bild ab (${gastAus.code})`);
|
|
const beimHost = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
|
|
ok(beimHost.j?.gaeste?.some((g) => g.id === IDS.gast2 && g.kamera_aus === true),
|
|
"und der Host sieht es an der Gastkarte");
|
|
|
|
/* Zurueck, damit der Rest der Pruefung von einem sauberen Stand
|
|
aus weiterlaeuft. */
|
|
await roh(CREW, "/workspace/api/reaktion/senden", {
|
|
method: "POST", keks: k.admin, rumpf: { kamera_aus: false, mikro_aus: false },
|
|
});
|
|
await roh(CREW, "/workspace/api/reaktion/senden", {
|
|
method: "POST", keks: k.gast2, rumpf: { kamera_aus: false },
|
|
});
|
|
|
|
/* ---- Chat ---- */
|
|
for (const rolle of ["linke", "modi", "gast"]) {
|
|
const a = await roh(CREW, "/workspace/api/reaktion/chatmodus", {
|
|
method: "POST", keks: k[rolle], rumpf: { modus: "zu" },
|
|
});
|
|
ok(a.code === 403, `${rolle}: stellt den Chat nicht um (${a.code})`);
|
|
}
|
|
const unsinn = await roh(CREW, "/workspace/api/reaktion/chatmodus", {
|
|
method: "POST", keks: k.admin, rumpf: { modus: "halboffen" },
|
|
});
|
|
ok(unsinn.code === 400 && unsinn.j?.fehler === "chatmodus_unbekannt",
|
|
`eine erfundene Stufe wird abgelehnt (${unsinn.code})`);
|
|
|
|
const offen = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast, rumpf: { text: "offen geht" },
|
|
});
|
|
ok(offen.code === 201, `bei „offen" schreibt die Community (${offen.code})`);
|
|
|
|
const team = await roh(CREW, "/workspace/api/reaktion/chatmodus", {
|
|
method: "POST", keks: k.hand, rumpf: { modus: "team" },
|
|
});
|
|
ok(team.code === 200 && team.j?.chat_modus === "team",
|
|
`die rechte Hand stellt auf „Team" (${team.j?.chat_modus})`);
|
|
|
|
const gastTeam = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast, rumpf: { text: "und jetzt?" },
|
|
});
|
|
ok(gastTeam.code === 403 && gastTeam.j?.fehler === "chat_nur_team",
|
|
`dann schweigt die Community (${gastTeam.code}/${gastTeam.j?.fehler})`);
|
|
const modiTeam = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.modi, rumpf: { text: "wir ordnen kurz" },
|
|
});
|
|
ok(modiTeam.code === 201, `ein Modi schon -- er soll ja ordnen (${modiTeam.code})`);
|
|
|
|
/* DIE OBERFLAECHE MUSS DIESELBE ANTWORT BEKOMMEN wie der Server.
|
|
Sonst steht ein Feld da, das sich beschreiben laesst und beim
|
|
Absenden 403 sagt -- eine Falle, keine Bedienung. */
|
|
const sichtGast = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast });
|
|
ok(sichtGast.j?.ich?.darf_schreiben === false,
|
|
"und die Oberflaeche erfaehrt es, bevor jemand tippt");
|
|
const sichtModi = await roh(CREW, "/workspace/api/reaktion", { keks: k.modi });
|
|
ok(sichtModi.j?.ich?.darf_schreiben === true,
|
|
"der Modi erfaehrt das Gegenteil");
|
|
|
|
const zu = await roh(CREW, "/workspace/api/reaktion/chatmodus", {
|
|
method: "POST", keks: k.admin, rumpf: { modus: "zu" },
|
|
});
|
|
ok(zu.j?.chat_modus === "zu", "die Stufe zu geht auch");
|
|
const modiZu = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.modi, rumpf: { text: "auch ich?" },
|
|
});
|
|
ok(modiZu.code === 403 && modiZu.j?.fehler === "chat_zu",
|
|
`dann schweigt auch der Modi (${modiZu.code}/${modiZu.j?.fehler})`);
|
|
const hostZu = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.admin, rumpf: { text: "ich sage noch etwas" },
|
|
});
|
|
ok(hostZu.code === 201,
|
|
`die zwei, die fuehren, koennen weiter reden (${hostZu.code})`);
|
|
|
|
/* GEGENPROBE: Es geht auch wieder auf. Eine Sperre, die man nicht
|
|
loesen kann, ist keine Stufe, sondern ein Ende. */
|
|
await roh(CREW, "/workspace/api/reaktion/chatmodus", {
|
|
method: "POST", keks: k.admin, rumpf: { modus: "offen" },
|
|
});
|
|
const wiederOffen = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast, rumpf: { text: "wieder da" },
|
|
});
|
|
ok(wiederOffen.code === 201,
|
|
`Gegenprobe: zurueck auf „offen", und die Community schreibt wieder (${wiederOffen.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
14. KEIN WEG HEISST ZWEIMAL GLEICH
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 14. Keine doppelten Wege ===");
|
|
{
|
|
/* Beim Notizblock hat genau das eine Stunde gekostet:
|
|
`DELETE /workspace/api/notizen/:id` gab es schon woanders, und
|
|
express nimmt schweigend die erste passende Route. */
|
|
const { default: app } = await import("./index.js");
|
|
const wege = [];
|
|
const sammeln = (stapel, vorn = "") => {
|
|
for (const s of stapel) {
|
|
if (s.route) {
|
|
for (const m of Object.keys(s.route.methods)) {
|
|
wege.push(`${m.toUpperCase()} ${vorn}${s.route.path}`);
|
|
}
|
|
} else if (s.handle?.stack) {
|
|
sammeln(s.handle.stack, vorn);
|
|
}
|
|
}
|
|
};
|
|
sammeln(app._router.stack);
|
|
ok(wege.length > 300, `${wege.length} Wege im Baum gefunden`);
|
|
const doppelt = wege.filter((x, i) => wege.indexOf(x) !== i);
|
|
ok(doppelt.length === 0,
|
|
doppelt.length ? `DOPPELT: ${[...new Set(doppelt)].join(", ")}` : "kein Weg kommt zweimal vor");
|
|
const reaktionswege = wege.filter((x) => x.includes("/api/reaktion")).length;
|
|
ok(reaktionswege >= 17, `die Reaction bringt ${reaktionswege} eigene Wege mit`);
|
|
}
|
|
|
|
|
|
/* =======================================================================
|
|
15. MODERATION IM LIVE-CHAT
|
|
|
|
Der heikelste Teil der ganzen Reaction: Hier kann jemand einem
|
|
anderen das Wort nehmen. Geprueft wird deshalb nicht nur, dass es
|
|
GEHT, sondern vor allem, wer es NICHT darf und was dabei nicht
|
|
herauskommen darf.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 15. Moderation im Live-Chat ===");
|
|
{
|
|
/* Sauberer Anfang: Der Chat steht auf „offen". */
|
|
await roh(CREW, "/workspace/api/reaktion/chatmodus", {
|
|
method: "POST", keks: k.admin, rumpf: { modus: "offen" },
|
|
});
|
|
|
|
/* ---- Wer eine Massnahme verhaengen darf ---- */
|
|
const zuschauer = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
|
method: "POST", keks: k.gast, rumpf: { person_id: IDS.gast2, art: "stumm", minuten: 10 },
|
|
});
|
|
ok(zuschauer.code === 403 && zuschauer.j?.fehler === "nur_moderation",
|
|
`ein Zuschauer schaltet niemanden stumm (${zuschauer.code}/${zuschauer.j?.fehler})`);
|
|
|
|
/* ---- Nicht gegen das Team ----
|
|
Wer einen Modi stummschalten koennte, haette einen Weg, die
|
|
Moderation selbst auszuschalten -- mitten in der Sendung. */
|
|
const gegenTeam = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
|
method: "POST", keks: k.modi, rumpf: { person_id: IDS.linke, art: "stumm", minuten: 10 },
|
|
});
|
|
ok(gegenTeam.code === 403 && gegenTeam.j?.fehler === "nicht_gegen_team",
|
|
`ein Modi schaltet die linke Hand nicht stumm (${gegenTeam.code}/${gegenTeam.j?.fehler})`);
|
|
const gegenHost = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
|
method: "POST", keks: k.modi, rumpf: { person_id: IDS.admin, art: "raus" },
|
|
});
|
|
ok(gegenHost.code === 403, `und DogFather erst recht nicht (${gegenHost.code})`);
|
|
|
|
/* ---- Die Dauer hat eine Obergrenze ---- */
|
|
const { STUMM_MAX_MINUTEN, MASSNAHMEN } = await import("./reaktion-tabellen.js");
|
|
for (const [min, wort] of [[0, "null"], [-5, "minus"], [STUMM_MAX_MINUTEN + 1, "zu lang"]]) {
|
|
const a = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
|
method: "POST", keks: k.modi, rumpf: { person_id: IDS.gast2, art: "stumm", minuten: min },
|
|
});
|
|
ok(a.code === 400 && a.j?.fehler === "dauer_unklar",
|
|
`${min} Minuten (${wort}) wird abgelehnt (${a.code})`);
|
|
}
|
|
const quatsch = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
|
method: "POST", keks: k.modi, rumpf: { person_id: IDS.gast2, art: "verbannen" },
|
|
});
|
|
ok(quatsch.code === 400 && quatsch.j?.fehler === "massnahme_unbekannt",
|
|
`eine erfundene Massnahme wird abgelehnt (${quatsch.code})`);
|
|
ok(MASSNAHMEN.length === 2 && MASSNAHMEN.includes("stumm") && MASSNAHMEN.includes("raus"),
|
|
`es gibt genau zwei kurze Massnahmen: ${MASSNAHMEN.join(", ")}`);
|
|
|
|
/* ---- Stumm wirkt -- und sagt den richtigen Grund ----
|
|
Der Chat steht dabei auf „offen". Genau das ist der Fall, der
|
|
vorher die falsche Auskunft gab („Der Chat ist gerade zu"). */
|
|
const vorher = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast2, rumpf: { text: "vorher geht es" },
|
|
});
|
|
ok(vorher.code === 201, `vor der Massnahme schreibt gast2 (${vorher.code})`);
|
|
|
|
const stumm = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
|
method: "POST", keks: k.modi, rumpf: { person_id: IDS.gast2, art: "stumm", minuten: 10 },
|
|
});
|
|
ok(stumm.code === 200, `ein Modi schaltet einen Zuschauer stumm (${stumm.code})`);
|
|
|
|
const nachher = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast2, rumpf: { text: "und jetzt?" },
|
|
});
|
|
ok(nachher.code === 403 && nachher.j?.fehler === "stumm_geschaltet",
|
|
`danach nicht mehr -- und der Grund stimmt (${nachher.code}/${nachher.j?.fehler})`);
|
|
|
|
const sichtGast2 = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast2 });
|
|
ok(sichtGast2.j?.ich?.darf_schreiben === false
|
|
&& sichtGast2.j?.ich?.schreib_grund === "stumm_geschaltet",
|
|
"und er steht auch in seinem eigenen Stand");
|
|
ok(sichtGast2.j?.ich?.massnahme?.art === "stumm" && !!sichtGast2.j.ich.massnahme.bis,
|
|
"mit Art und Ablaufzeitpunkt");
|
|
|
|
/* ZUSEHEN GEHT WEITER. „stumm" ist nicht „raus" -- wer das
|
|
verwechselt, wirft jemanden fuer einen Satz aus der Sendung. */
|
|
const nochDabei = await roh(CREW, "/workspace/api/reaktion/dabei", {
|
|
method: "POST", keks: k.gast2,
|
|
});
|
|
ok(nochDabei.code === 200, `zusehen geht weiter (${nochDabei.code})`);
|
|
|
|
/* ---- Die Massnahmen sind keine oeffentliche Liste ----
|
|
In `meldungen` steht der Name dessen, der gemeldet hat. Wer
|
|
meldet, muss sich darauf verlassen koennen, dass das niemand
|
|
sieht -- sonst meldet beim naechsten Mal keiner mehr. */
|
|
ok(Array.isArray(stumm.j?.massnahmen) && stumm.j.massnahmen.length >= 1,
|
|
"die Moderation sieht die laufenden Massnahmen");
|
|
ok(sichtGast2.j?.massnahmen === null && sichtGast2.j?.meldungen === null,
|
|
"ein Zuschauer sieht weder Massnahmen noch Meldungen");
|
|
const sichtModi = await roh(CREW, "/workspace/api/reaktion", { keks: k.modi });
|
|
ok(Array.isArray(sichtModi.j?.meldungen), "ein Modi schon");
|
|
|
|
/* GEGENPROBE AM RUNDRUF: `oeffentlich()` muss beide Felder
|
|
wegnehmen. Es aus dem Quelltext zu lesen ist hier genauer als
|
|
jede Messung -- der Rundruf geht an alle, und ein Fehler darin
|
|
waere erst aufgefallen, wenn jemand mitgelesen haette. */
|
|
{
|
|
const quelle = readFileSync(join(HIER, "workspace-reaktion.js"), "utf8");
|
|
const zeile = quelle.match(/const \{([^}]*)\} = l;/);
|
|
const weg = (zeile ? zeile[1] : "").split(",").map((x) => x.trim().replace(/\.\.\..*/, ""));
|
|
for (const feld of ["zuschauer", "ich", "liste", "meldungen", "massnahmen"]) {
|
|
ok(weg.includes(feld), `oeffentlich() nimmt „${feld}" aus dem Rundruf`);
|
|
}
|
|
}
|
|
|
|
/* ---- Wieder aufheben ---- */
|
|
const auf = await roh(CREW, `/workspace/api/reaktion/massnahme/${IDS.gast2}`, {
|
|
method: "DELETE", keks: k.gast,
|
|
});
|
|
ok(auf.code === 403, `ein Zuschauer hebt sie nicht auf (${auf.code})`);
|
|
const auf2 = await roh(CREW, `/workspace/api/reaktion/massnahme/${IDS.gast2}`, {
|
|
method: "DELETE", keks: k.modi,
|
|
});
|
|
ok(auf2.code === 200, `ein Modi schon (${auf2.code})`);
|
|
const wieder = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast2, rumpf: { text: "wieder da" },
|
|
});
|
|
ok(wieder.code === 201, `Gegenprobe: danach schreibt er wieder (${wieder.code})`);
|
|
|
|
/* ---- Melden ---- */
|
|
const zuMelden = wieder.j.id;
|
|
const selbst = await roh(CREW, `/workspace/api/reaktion/chat/${zuMelden}/melden`, {
|
|
method: "POST", keks: k.gast2, rumpf: {},
|
|
});
|
|
ok(selbst.code === 400 && selbst.j?.fehler === "eigener_beitrag",
|
|
`den eigenen Beitrag meldet man nicht (${selbst.code})`);
|
|
|
|
const gemeldet = await roh(CREW, `/workspace/api/reaktion/chat/${zuMelden}/melden`, {
|
|
method: "POST", keks: k.gast, rumpf: { grund: "unfreundlich" },
|
|
});
|
|
ok(gemeldet.code === 201, `ein Zuschauer darf melden (${gemeldet.code})`);
|
|
|
|
/* ZWEIMAL MELDEN IST EINMAL. Ohne die Sperre koennte einer allein
|
|
die Liste fuellen, und die Moderation sieht die echte nicht. */
|
|
const nochmal = await roh(CREW, `/workspace/api/reaktion/chat/${zuMelden}/melden`, {
|
|
method: "POST", keks: k.gast, rumpf: {},
|
|
});
|
|
ok(nochmal.code === 200 && nochmal.j?.schon === true,
|
|
`zweimal melden zaehlt einmal -- ohne Fehlermeldung (${nochmal.code})`);
|
|
|
|
const beiModi = await roh(CREW, "/workspace/api/reaktion", { keks: k.modi });
|
|
const m = (beiModi.j?.meldungen || []).find((x) => x.beitrag_id === zuMelden);
|
|
ok(!!m, "die Meldung steht bei der Moderation");
|
|
ok(m?.text === "wieder da" && m?.wer_id === IDS.gast2,
|
|
"mit dem Text und der Person, um die es geht");
|
|
|
|
/* ---- Erledigen ---- */
|
|
const nein = await roh(CREW, `/workspace/api/reaktion/meldung/${m.id}/erledigt`, {
|
|
method: "POST", keks: k.gast,
|
|
});
|
|
ok(nein.code === 403, `ein Zuschauer erledigt keine Meldung (${nein.code})`);
|
|
const ja = await roh(CREW, `/workspace/api/reaktion/meldung/${m.id}/erledigt`, {
|
|
method: "POST", keks: k.modi,
|
|
});
|
|
ok(ja.code === 200, `ein Modi schon (${ja.code})`);
|
|
ok(!(ja.j?.meldungen || []).some((x) => x.id === m.id),
|
|
"und sie ist aus der offenen Liste");
|
|
const zweimal = await roh(CREW, `/workspace/api/reaktion/meldung/${m.id}/erledigt`, {
|
|
method: "POST", keks: k.modi,
|
|
});
|
|
ok(zweimal.code === 409 && zweimal.j?.fehler === "nicht_offen",
|
|
`zweimal erledigen geht nicht (${zweimal.code})`);
|
|
|
|
/* ---- „raus" heisst raus ---- */
|
|
const raus = await roh(CREW, "/workspace/api/reaktion/massnahme", {
|
|
method: "POST", keks: k.hand, rumpf: { person_id: IDS.gast2, art: "raus" },
|
|
});
|
|
ok(raus.code === 200, `die rechte Hand nimmt jemanden aus der Sendung (${raus.code})`);
|
|
const rein = await roh(CREW, "/workspace/api/reaktion/dabei", {
|
|
method: "POST", keks: k.gast2,
|
|
});
|
|
ok(rein.code === 403 && rein.j?.fehler === "aus_der_sendung",
|
|
`er kommt nicht mehr herein -- und erfaehrt warum (${rein.code}/${rein.j?.fehler})`);
|
|
|
|
const weg2 = await roh(CREW, "/workspace/api/reaktion/chat", {
|
|
method: "POST", keks: k.gast2, rumpf: { text: "hallo?" },
|
|
});
|
|
ok(weg2.code === 403 && weg2.j?.fehler === "aus_der_sendung",
|
|
`und schreiben auch nicht (${weg2.code}/${weg2.j?.fehler})`);
|
|
|
|
await roh(CREW, `/workspace/api/reaktion/massnahme/${IDS.gast2}`, {
|
|
method: "DELETE", keks: k.admin,
|
|
});
|
|
const zurueck = await roh(CREW, "/workspace/api/reaktion/dabei", {
|
|
method: "POST", keks: k.gast2,
|
|
});
|
|
ok(zurueck.code === 200, `Gegenprobe: aufgehoben, und er ist wieder dabei (${zurueck.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
16. WAS DIE SEITE DARAUS MACHT
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 16. Rollen und Moderation auf der Seite ===");
|
|
{
|
|
const js = readFileSync(join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8");
|
|
const css = readFileSync(join(HIER, "..", "workspace", "assets", "css", "reaktion.css"), "utf8");
|
|
const html = readFileSync(join(HIER, "..", "workspace", "reaktion.html"), "utf8");
|
|
const saetze = readFileSync(join(HIER, "..", "workspace", "assets", "js", "meldung.js"), "utf8");
|
|
|
|
/* JEDE ROLLE MIT EIGENEM AUSSEHEN BEKOMMT AUCH EINE REGEL.
|
|
Ein Abzeichen ohne Stil ist ein Wort ohne Kasten -- es faellt
|
|
nicht auf, und damit ist der ganze Zweck weg. */
|
|
const abzeichen = [...js.matchAll(/^ {4}(admin|hand|linke|modi): \['([^']+)', '([^']+)'\],/gm)];
|
|
ok(abzeichen.length === 4, `vier Rollen tragen ein Abzeichen (${abzeichen.length})`);
|
|
for (const [, rolle, wort, marke] of abzeichen) {
|
|
ok(wort.length >= 3 && wort.length <= 6, `„${wort}" fuer ${rolle} ist kurz genug`);
|
|
ok(css.includes(`.beitrag__rolle[data-rolle="${marke}"]`),
|
|
`und „${marke}" hat eine eigene Farbe`);
|
|
}
|
|
/* GEGENPROBE: Kann diese Pruefung „nein" sagen? */
|
|
ok(!css.includes('.beitrag__rolle[data-rolle="gibtesnicht"]'),
|
|
"Gegenprobe: eine erfundene Marke hat keine Regel");
|
|
|
|
/* DAS WORT IST NICHT DER RANG. Filipe steht nie ueber seinem Team --
|
|
Woerter wie „Chef" oder „Leitung" haetten hier nichts zu suchen. */
|
|
const rangwoerter = ["Chef", "Boss", "Leiter", "Leitung", "Kopf"];
|
|
for (const [, , wort] of abzeichen) {
|
|
ok(!rangwoerter.includes(wort), `„${wort}" ist kein Rang`);
|
|
}
|
|
|
|
ok(html.includes('id="chat-meldungen"'), "der Meldungs-Knopf steht in der Seite");
|
|
ok(css.includes(".schiene__meldungen"), "und hat eine Regel");
|
|
ok(js.includes("meldungenZeichnen()") && js.includes("meldungenZeigen"),
|
|
"die Seite zeichnet und oeffnet ihn");
|
|
|
|
/* DIE TIPPZIELE IM MODERATIONSMENUE. Hier wird jemand stumm
|
|
geschaltet; ein Fehlgriff trifft den Falschen. */
|
|
const knopf = css.match(/\.modmenue__knopf \{[^}]*\}/);
|
|
ok(!!knopf && /min-height:\s*44px/.test(knopf[0]),
|
|
"jeder Knopf im Moderationsmenue ist 44 px hoch");
|
|
|
|
/* JEDES FEHLERWORT DES SERVERS HAT EINEN DEUTSCHEN SATZ.
|
|
Sonst steht im Chat ein Maschinenwort wie „nicht_gegen_team". */
|
|
const server = readFileSync(join(HIER, "workspace-reaktion.js"), "utf8");
|
|
const woerter = [...new Set([...server.matchAll(/fehler:\s*"([a-z_0-9]+)"/g)]
|
|
.map((x) => x[1]))].filter((w) => w !== "nicht_verfuegbar");
|
|
ok(woerter.length > 20, `${woerter.length} Fehlerwoerter im Server gefunden`);
|
|
const ohneSatz = woerter.filter((w) => !new RegExp(`^\\s+${w}:`, "m").test(saetze));
|
|
ok(ohneSatz.length === 0,
|
|
ohneSatz.length ? `OHNE SATZ: ${ohneSatz.join(", ")}` : "alle haben einen deutschen Satz");
|
|
/* Die Gegenprobe braucht ein Wort, das es WIRKLICH nicht gibt --
|
|
„gibt_es_nicht" steht seit Langem in der Tafel und liess die
|
|
Gegenprobe genau das melden, wovor sie warnen sollte. */
|
|
ok(!/^\s+zwetschgenknoedel:/m.test(saetze),
|
|
"Gegenprobe: ein erfundenes Wort hat keinen Satz");
|
|
|
|
/* KEIN SCHLUESSEL STEHT ZWEIMAL IN DER SATZTAFEL.
|
|
Ein zweiter Eintrag mit demselben Namen gewinnt stillschweigend --
|
|
genau so stand am 28.09. in der Hilfe „Der Saal ist zu". */
|
|
const schluessel = [...saetze.matchAll(/^ {4}([a-z_0-9]+):/gm)].map((x) => x[1]);
|
|
const doppelt = [...new Set(schluessel.filter((x, i) => schluessel.indexOf(x) !== i))];
|
|
ok(doppelt.length === 0,
|
|
doppelt.length ? `DOPPELT: ${doppelt.join(", ")}` : `${schluessel.length} Saetze, keiner doppelt`);
|
|
}
|
|
|
|
|
|
/* =======================================================================
|
|
17. DAS VIDEO: TEMPO, ANORDNUNG, DAS ZWEITE
|
|
|
|
Filipe: „auch bei den videos sachen wie pause geschwindigkeit.
|
|
meine kamera groesser machen video kleiner. nur mein bild, nur das
|
|
video ... auch gerne so dass ich vielleicht noch ein zweites
|
|
nebenbei vorbereiten kann so dass ich hin und her switchen kann."
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 17. Tempo, Anordnung und das zweite Video ===");
|
|
{
|
|
const { TEMPI, LAYOUTS } = await import("./reaktion-tabellen.js");
|
|
|
|
/* ---- Die Anordnungen ---- */
|
|
ok(LAYOUTS.length === 5 && LAYOUTS.includes("nur_video") && LAYOUTS.includes("nur_kamera"),
|
|
`es gibt fuenf Anordnungen: ${LAYOUTS.join(", ")}`);
|
|
for (const l of LAYOUTS) {
|
|
const a = await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.admin, rumpf: { layout: l },
|
|
});
|
|
ok(a.code === 200 && a.j?.layout === l, `„${l}" laesst sich stellen (${a.code})`);
|
|
}
|
|
const quatschL = await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.admin, rumpf: { layout: "diagonal" },
|
|
});
|
|
ok(quatschL.code === 400 && quatschL.j?.fehler === "layout_unbekannt",
|
|
`eine erfundene Anordnung wird abgelehnt (${quatschL.code})`);
|
|
const nichtHost = await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.modi, rumpf: { layout: "kino" },
|
|
});
|
|
ok(nichtHost.code === 403, `ein Modi stellt die Anordnung nicht (${nichtHost.code})`);
|
|
await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.admin, rumpf: { layout: "kino" },
|
|
});
|
|
|
|
/* JEDE ANORDNUNG BRAUCHT AUCH EINE REGEL -- sonst ist sie ein
|
|
Knopf, der nichts tut. „kino" ist die Vorgabe und braucht keine. */
|
|
{
|
|
const css = readFileSync(join(HIER, "..", "workspace", "assets", "css", "reaktion.css"), "utf8");
|
|
const ohne = LAYOUTS.filter((l) => l !== "kino")
|
|
.filter((l) => !css.includes(`[data-layout="${l}"]`));
|
|
ok(ohne.length === 0,
|
|
ohne.length ? `OHNE REGEL: ${ohne.join(", ")}` : "jede Anordnung hat eine eigene Regel");
|
|
ok(!css.includes('[data-layout="diagonal"]'),
|
|
"Gegenprobe: eine erfundene Anordnung hat keine");
|
|
/* KEIN `opacity: 0` FUER DAS WEGLASSEN. Ein unsichtbarer
|
|
YouTube-Rahmen spielt weiter -- man hoerte ein Video, das
|
|
niemand sieht. */
|
|
const nurKamera = css.match(/\.kino\[data-layout="nur_kamera"\] \.leinwand__video \{[^}]*\}/);
|
|
ok(!!nurKamera && /display:\s*none/.test(nurKamera[0]),
|
|
"„nur Kamera“ nimmt das Video ganz weg und macht es nicht nur durchsichtig");
|
|
}
|
|
|
|
/* ---- Die Geschwindigkeit ---- */
|
|
ok(Array.isArray(TEMPI) && TEMPI.includes(1) && TEMPI.includes(0.5) && TEMPI.includes(2),
|
|
`es gibt ${TEMPI.length} Geschwindigkeiten: ${TEMPI.join(", ")}`);
|
|
/* YouTube kann 0,25 bis 2. Alles ausserhalb waere ein Knopf, den
|
|
der Spieler schweigend ignoriert -- und dann steht die Regie auf
|
|
3x und es laeuft normal. */
|
|
ok(TEMPI.every((t) => t >= 0.25 && t <= 2), "und alle liegen in dem, was YouTube kann");
|
|
|
|
for (const t of [1.5, 0.5, 1]) {
|
|
const a = await roh(CREW, "/workspace/api/reaktion/video", {
|
|
method: "POST", keks: k.admin, rumpf: { tempo: t, laeuft: true, sekunde: 12 },
|
|
});
|
|
ok(a.code === 200 && a.j?.tempo === t, `${t}-fach laesst sich stellen (${a.code})`);
|
|
}
|
|
for (const t of [3, 0.1, "schnell", 1.1]) {
|
|
const a = await roh(CREW, "/workspace/api/reaktion/video", {
|
|
method: "POST", keks: k.admin, rumpf: { tempo: t, laeuft: true, sekunde: 12 },
|
|
});
|
|
ok(a.code === 400 && a.j?.fehler === "tempo_unklar",
|
|
`„${t}" wird abgelehnt (${a.code})`);
|
|
}
|
|
|
|
/* DER TAKT DARF DAS TEMPO NICHT ZURUECKDREHEN. Die Videoroute
|
|
laeuft alle fuenf Sekunden; schriebe sie das Tempo jedes Mal mit,
|
|
wuerde ein Takt mit altem Wert die Einstellung ueberschreiben. */
|
|
await roh(CREW, "/workspace/api/reaktion/video", {
|
|
method: "POST", keks: k.admin, rumpf: { tempo: 1.5, laeuft: true, sekunde: 20 },
|
|
});
|
|
await roh(CREW, "/workspace/api/reaktion/video", {
|
|
method: "POST", keks: k.admin, rumpf: { laeuft: true, sekunde: 25 },
|
|
});
|
|
const nachTakt = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast });
|
|
ok(nachTakt.j?.tempo === 1.5,
|
|
`ein Takt ohne Tempo laesst es stehen (${nachTakt.j?.tempo})`);
|
|
|
|
/* UND ES GEHT AN ALLE. Wer sie nicht bekaeme, saehe dasselbe Video
|
|
in einem anderen Tempo. */
|
|
ok(nachTakt.j?.tempo === 1.5 && Array.isArray(nachTakt.j?.tempi),
|
|
"auch ein Zuschauer erfaehrt Tempo und Auswahl");
|
|
await roh(CREW, "/workspace/api/reaktion/video", {
|
|
method: "POST", keks: k.admin, rumpf: { tempo: 1, laeuft: true, sekunde: 25 },
|
|
});
|
|
|
|
/* ---- Das zweite Video ---- */
|
|
const A = "dQw4w9WgXcQ";
|
|
const B = "9bZkp7q19f0";
|
|
await roh(CREW, "/workspace/api/reaktion/video", {
|
|
method: "POST", keks: k.admin, rumpf: { video: A, laeuft: true, sekunde: 0 },
|
|
});
|
|
|
|
const nurHost2 = await roh(CREW, "/workspace/api/reaktion/zweitvideo", {
|
|
method: "POST", keks: k.modi, rumpf: { video: B },
|
|
});
|
|
ok(nurHost2.code === 403, `ein Modi legt kein zweites Video bereit (${nurHost2.code})`);
|
|
|
|
const selbes = await roh(CREW, "/workspace/api/reaktion/zweitvideo", {
|
|
method: "POST", keks: k.admin, rumpf: { video: A },
|
|
});
|
|
ok(selbes.code === 409 && selbes.j?.fehler === "schon_im_bild",
|
|
`dasselbe Video auf beide Seiten geht nicht (${selbes.code})`);
|
|
|
|
const ohneZweites = await roh(CREW, "/workspace/api/reaktion/tauschen", {
|
|
method: "POST", keks: k.admin, rumpf: {},
|
|
});
|
|
ok(ohneZweites.code === 409 && ohneZweites.j?.fehler === "kein_zweites",
|
|
`ohne zweites Video gibt es nichts zu tauschen (${ohneZweites.code})`);
|
|
|
|
const gelegt = await roh(CREW, "/workspace/api/reaktion/zweitvideo", {
|
|
method: "POST", keks: k.admin, rumpf: { video: `https://youtu.be/${B}` },
|
|
});
|
|
ok(gelegt.code === 200 && gelegt.j?.zweit?.video === B,
|
|
`eine ganze Adresse wird verstanden (${gelegt.j?.zweit?.video})`);
|
|
|
|
/* ES GEHT NICHT AN ALLE. Was daneben vorbereitet ist, ist die
|
|
Rueckhand -- die verraet man nicht vorher. */
|
|
const beimGast = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast });
|
|
ok(beimGast.j?.zweit === null, "ein Zuschauer sieht das zweite Video nicht");
|
|
{
|
|
const quelle = readFileSync(join(HIER, "workspace-reaktion.js"), "utf8");
|
|
const zeile = quelle.match(/const \{([^}]*)\} = l;/);
|
|
ok((zeile ? zeile[1] : "").includes("zweit"),
|
|
"und es steht auch nicht im Rundruf");
|
|
}
|
|
|
|
/* ---- Hin und her, und jedes merkt sich seine Stelle ---- */
|
|
const hin = await roh(CREW, "/workspace/api/reaktion/tauschen", {
|
|
method: "POST", keks: k.admin, rumpf: { sekunde: 87.5 },
|
|
});
|
|
ok(hin.code === 200 && hin.j?.video === B, `getauscht: jetzt laeuft B (${hin.j?.video})`);
|
|
ok(hin.j?.zweit?.video === A && Math.abs((hin.j?.zweit?.sekunde || 0) - 87.5) < 0.01,
|
|
`und A liegt bei 87,5 s bereit (${hin.j?.zweit?.sekunde})`);
|
|
ok(Math.abs((hin.j?.sekunde || 0) - 0) < 0.01,
|
|
`B faengt bei 0 an -- es lief noch nie (${hin.j?.sekunde})`);
|
|
|
|
const zurueck = await roh(CREW, "/workspace/api/reaktion/tauschen", {
|
|
method: "POST", keks: k.admin, rumpf: { sekunde: 31 },
|
|
});
|
|
ok(zurueck.code === 200 && zurueck.j?.video === A, `zurueck: wieder A (${zurueck.j?.video})`);
|
|
ok(Math.abs((zurueck.j?.sekunde || 0) - 87.5) < 0.01,
|
|
`und zwar GENAU DORT, wo es aufgehoert hat: ${zurueck.j?.sekunde} s`);
|
|
ok(Math.abs((zurueck.j?.zweit?.sekunde || 0) - 31) < 0.01,
|
|
`B merkt sich jetzt 31 s (${zurueck.j?.zweit?.sekunde})`);
|
|
|
|
/* GEGENPROBE: Ohne die gemerkte Stelle waere „hin und her" ein
|
|
Neustart von vorn -- und dann schaltet niemand zweimal. */
|
|
ok((zurueck.j?.sekunde || 0) > 1,
|
|
"Gegenprobe: die Stelle ist nicht einfach null");
|
|
|
|
/* ---- Wegraeumen ---- */
|
|
const weg = await roh(CREW, "/workspace/api/reaktion/zweitvideo", {
|
|
method: "POST", keks: k.admin, rumpf: { video: "" },
|
|
});
|
|
ok(weg.code === 200 && weg.j?.zweit === null, `ein leeres Feld raeumt es weg (${weg.code})`);
|
|
const danach = await roh(CREW, "/workspace/api/reaktion/tauschen", {
|
|
method: "POST", keks: k.admin, rumpf: {},
|
|
});
|
|
ok(danach.code === 409, `und danach gibt es nichts mehr zu tauschen (${danach.code})`);
|
|
|
|
/* ---- Und die Seite? ---- */
|
|
{
|
|
const js = readFileSync(join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8");
|
|
const buehne = readFileSync(join(HIER, "..", "workspace", "assets", "js", "buehne.js"), "utf8");
|
|
const html = readFileSync(join(HIER, "..", "workspace", "reaktion.html"), "utf8");
|
|
ok(html.includes('id="tempo"') && html.includes('id="v-tauschen"')
|
|
&& html.includes('id="p-zweit"'),
|
|
"die Regie hat Tempo, Umschalter und das Feld fuers zweite Video");
|
|
/* BEIDE SPIELER MUESSEN DAS TEMPO NACHZIEHEN -- der im Saal und
|
|
der in OBS. Zieht nur einer nach, laeuft der Stream anders als
|
|
der Saal, und das faellt erst nach Minuten auf. */
|
|
for (const [name, quelle] of [["die Seite", js], ["die Buehne", buehne]]) {
|
|
ok(/setPlaybackRate/.test(quelle), `${name} stellt die Geschwindigkeit`);
|
|
/* Und nach einem Videowechsel nochmal: YouTube setzt sie beim
|
|
Laden auf 1 zurueck. */
|
|
ok(/loadVideoById[\s\S]{0,400}tempoNachziehen/.test(quelle),
|
|
`${name} zieht sie auch nach einem Videowechsel nach`);
|
|
ok(/getAvailablePlaybackRates/.test(quelle),
|
|
`${name} setzt keinen Wert, den der Spieler nicht kann`);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* =======================================================================
|
|
18. 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."
|
|
|
|
Geprueft wird hier, was sich aus dem Quelltext beweisen laesst --
|
|
Lage, Verdrahtung, Versprechen. Wie hoch sie ist und ob die Marke
|
|
sitzt, misst `mess-reaktion` im Browser; das kann kein Quelltext.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 18. Die Regieleiste ===");
|
|
{
|
|
const html = readFileSync(join(HIER, "..", "workspace", "reaktion.html"), "utf8");
|
|
const css = readFileSync(join(HIER, "..", "workspace", "assets", "css", "reaktion.css"), "utf8");
|
|
const js = readFileSync(join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8");
|
|
|
|
/* ---- SIE STEHT UEBER DEM SAAL, NICHT DARIN ----
|
|
Die Reihenfolge im Dokument ist die Aussage. Waere sie wieder im
|
|
Pult, stuende sie weiter unten -- und niemandem fiele es auf,
|
|
solange sie „irgendwo" da ist. */
|
|
const iLeiste = html.indexOf('id="regieleiste"');
|
|
const iKino = html.indexOf('id="teil-live"');
|
|
const iPult = html.indexOf('id="pult"');
|
|
ok(iLeiste > 0, "es gibt die Regieleiste");
|
|
ok(iLeiste < iKino, `sie steht vor dem Kino (${iLeiste} < ${iKino})`);
|
|
ok(iLeiste < iPult, "und vor dem Pult");
|
|
ok(html.indexOf('id="reiter"') < iPult,
|
|
"die Register stehen nicht mehr im Pult");
|
|
|
|
/* ---- JEDES REGISTER ZEIGT AUF EINE TAFEL, DIE ES GIBT ----
|
|
`aria-controls` ist ein Versprechen an Hilfsmittel. Zeigt es ins
|
|
Leere, sagt der Vorleser eine Tafel an, die niemand oeffnen
|
|
kann -- und gemerkt haette es nie jemand. */
|
|
const reiter = [...html.matchAll(/data-tafel="([a-z]+)"[^>]*aria-controls="([^"]+)"/g)]
|
|
.concat([...html.matchAll(/aria-controls="([^"]+)"[^>]*data-tafel="([a-z]+)"/g)]
|
|
.map((m) => [m[0], m[2], m[1]]));
|
|
ok(reiter.length >= 7, `${reiter.length} Register gefunden`);
|
|
const ohneTafel = reiter.filter(([, , ziel]) => !html.includes(`id="${ziel}"`));
|
|
ok(ohneTafel.length === 0,
|
|
ohneTafel.length
|
|
? `ZEIGT INS LEERE: ${ohneTafel.map((x) => x[2]).join(", ")}`
|
|
: "jedes Register zeigt auf eine Tafel, die es gibt");
|
|
ok(!html.includes('id="tafel-gibtesnicht"'),
|
|
"Gegenprobe: eine erfundene Tafel gibt es nicht");
|
|
|
|
/* ---- DAS VERSPRECHEN VON `role=tablist` ----
|
|
Steht die Rolle im Markup und loest das Programm sie nicht ein,
|
|
sagt ein Vorleser etwas an, was nicht stimmt. Das ist schlimmer
|
|
als gar keine Rolle. */
|
|
ok(/id="reiter"[^>]*role="tablist"/.test(html), "die Leiste ist eine Registerleiste");
|
|
/* Die Anfuehrungszeichen bleiben draussen -- im Quelltext steht
|
|
`'Home'`, in dieser Pruefung stand zuerst `"Home"`, und daran
|
|
ist sie gescheitert. Eine Pruefung, die die Schreibweise der
|
|
Anfuehrungszeichen mitprueft, prueft die falsche Sache. */
|
|
for (const taste of ["ArrowRight", "ArrowLeft", "Home", "End"]) {
|
|
ok(js.includes(taste), `das Programm kennt „${taste}"`);
|
|
}
|
|
ok(/tabIndex\s*=\s*gewaehlt \? 0 : -1/.test(js),
|
|
"genau ein Register liegt in der Tabulatorfolge");
|
|
|
|
/* ---- EIN REGISTER MACHT DIE ZUGEKLAPPTE REGIE AUF ----
|
|
Seit die Leiste oben steht, kann das Pult zu sein. Ohne diese
|
|
Zeile waere jedes Register bei zugeklappter Regie ein Knopf,
|
|
bei dem sichtbar nichts passiert. */
|
|
ok(/pult\.dataset\.auf = 'ja'/.test(js) && /reiterWechseln/.test(js),
|
|
"ein Register klappt die Regie auf");
|
|
|
|
/* ---- DIE MARKE UND IHRE ZWEITE ANZEIGE ----
|
|
Was gerechnet wird, braucht etwas daneben, das nicht rechnet. */
|
|
ok(html.includes('id="reiter-marke"'), "es gibt die gleitende Marke");
|
|
ok(/offsetLeft/.test(js) && /offsetWidth/.test(js) && /offsetHeight/.test(js),
|
|
"sie wird aus gemessenen Werten gesetzt, nicht gerechnet");
|
|
const knopfRegel = css.match(/\.reiter__knopf\[aria-selected="true"\] \{[^}]*\}/);
|
|
ok(!!knopfRegel && /color:/.test(knopfRegel[0]),
|
|
"das gewaehlte Register traegt ausserdem eine eigene Farbe");
|
|
|
|
/* ---- SIE GEHOERT NICHT IN DEN STREAM ----
|
|
Der Buehnenmodus zeigt nur, was Zuschauer sehen sollen. Eine
|
|
Regieleiste im Stream waere die Bedienung im Bild. */
|
|
ok(/body\[data-nur="buehne"\][^{]*\.regieleiste/.test(css.replace(/\s+/g, " ")),
|
|
"im Buehnenmodus ist sie weg");
|
|
|
|
/* ---- NUR EINE STELLE SAGT, WIE HOCH DIE ZEILEN SIND ----
|
|
|
|
Bis zum 28.09. standen zwei Angaben in derselben Datei; die
|
|
spaetere gewann und gab der Leiste null Pixel.
|
|
|
|
DIESE PRUEFUNG WAR ZU ENG UND HAT DESHALB DAS FALSCHE GEPRUEFT.
|
|
Sie verlangte, dass die Zeile mit `.saal` BEGINNT -- und hat
|
|
damit `body[data-nur="buehne"] .saal { grid-template-rows: … }`
|
|
nie gesehen. Die stand die ganze Zeit da, gab dem Saal im
|
|
Buehnenmodus eine einzige Zeile, und das auf Zeile 2 genagelte
|
|
Video landete in einer impliziten: Leinwand null Pixel hoch, im
|
|
Stream ein schwarzes Bild. Gemeldet hat es `mess-buehne`, nicht
|
|
diese Pruefung -- sie war gruen und wertlos.
|
|
|
|
Jetzt zaehlt JEDE Regel, in deren Auswahl `.saal` vorkommt. */
|
|
const zeilen = [...css.matchAll(/\{|\}/g)].length >= 0
|
|
? [...css.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
.matchAll(/([^{}]*\.saal[^{}]*)\{([^}]*)\}/g)]
|
|
.filter((m) => /grid-template-rows/.test(m[2]))
|
|
: [];
|
|
ok(zeilen.length === 1,
|
|
zeilen.length === 1
|
|
? `genau eine Regel bestimmt die Zeilen des Saals (${zeilen[0][1].trim()})`
|
|
: `${zeilen.length} Regeln bestimmen die Zeilen des Saals -- `
|
|
+ `eine davon wird irgendwo ignoriert: `
|
|
+ zeilen.map((m) => m[1].trim()).join(" / "));
|
|
/* GEGENPROBE: Die Pruefung muss eine zweite Regel auch dann
|
|
finden, wenn sie nicht am Zeilenanfang steht -- genau daran ist
|
|
sie gescheitert. */
|
|
{
|
|
const erfunden = 'body[data-nur="probe"] .saal { grid-template-rows: 1fr; }';
|
|
const mit = [...(css + erfunden).replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
.matchAll(/([^{}]*\.saal[^{}]*)\{([^}]*)\}/g)]
|
|
.filter((m) => /grid-template-rows/.test(m[2]));
|
|
ok(mit.length === 2,
|
|
`Gegenprobe: eine eingeschobene zweite Regel faellt auf (${mit.length})`);
|
|
}
|
|
|
|
/* ---- UND SIE GEHOERT NUR DENEN, DIE SENDEN ---- */
|
|
ok(/leiste\.hidden = !lage\.ich\.host/.test(js),
|
|
"nur wer die Sendung fuehrt, sieht sie");
|
|
}
|
|
|
|
|
|
/* =======================================================================
|
|
19. GROESSE UND ECKE DER KAMERAFENSTER
|
|
|
|
Filipe, 28.09.2026: „und danach perfektionierst du auch die
|
|
verschiedenen groessen von kamera und so."
|
|
|
|
Die Anordnung sagt das VERHAELTNIS von Video und Kameras. Was
|
|
fehlte, sind die zwei Dinge daneben: wie gross genau, und wo.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 19. Groesse und Ecke der Kamerafenster ===");
|
|
{
|
|
const { KAMERA_GROESSEN, KAMERA_ECKEN } = await import("./reaktion-tabellen.js");
|
|
|
|
/* ---- DIE LISTEN SELBST ----
|
|
Sechs Stufen statt eines Schiebers: Ein Schieber verspricht eine
|
|
stufenlose Einstellung, und dann sucht mitten in der Sendung
|
|
jemand nach „dem richtigen Wert", statt zu senden. */
|
|
ok(KAMERA_GROESSEN.length >= 4 && KAMERA_GROESSEN.includes(1),
|
|
`es gibt ${KAMERA_GROESSEN.length} Groessen: ${KAMERA_GROESSEN.join(", ")}`);
|
|
ok(KAMERA_GROESSEN.every((g) => g >= 0.5 && g <= 2),
|
|
"und alle liegen zwischen halb und doppelt");
|
|
/* Aufsteigend, weil die Knoepfe in dieser Reihenfolge stehen. Eine
|
|
Reihe, in der 1,5 vor 0,8 steht, sieht aus wie ein Fehler und
|
|
ist keiner -- man sucht trotzdem danach. */
|
|
ok(KAMERA_GROESSEN.every((g, i) => i === 0 || g > KAMERA_GROESSEN[i - 1]),
|
|
"die Groessen stehen aufsteigend");
|
|
ok(KAMERA_ECKEN.length === 4 && ["ol", "or", "ul", "ur"].every((e) => KAMERA_ECKEN.includes(e)),
|
|
`und vier Ecken: ${KAMERA_ECKEN.join(", ")}`);
|
|
|
|
/* ---- DIE VORGABE DER DATENBANK MUSS IN DER LISTE STEHEN ----
|
|
Sonst startet eine frische Anlage mit einem Wert, den der Server
|
|
selbst ablehnen wuerde -- und der erste Klick auf einen anderen
|
|
Knopf waere die einzige Gelegenheit, es zu merken. */
|
|
{
|
|
const tab = readFileSync(join(HIER, "reaktion-tabellen.js"), "utf8");
|
|
const vGross = tab.match(/kamera_gross REAL NOT NULL DEFAULT ([\d.]+)/);
|
|
const vEcke = tab.match(/kamera_ecke TEXT NOT NULL DEFAULT '([a-z]+)'/);
|
|
ok(!!vGross && KAMERA_GROESSEN.includes(Number(vGross[1])),
|
|
`die Vorgabe der Spalte (${vGross?.[1]}) steht in der Liste`);
|
|
ok(!!vEcke && KAMERA_ECKEN.includes(vEcke[1]),
|
|
`und die Vorgabe der Ecke (${vEcke?.[1]}) auch`);
|
|
}
|
|
|
|
/* ---- JEDE GROESSE LAESST SICH STELLEN ---- */
|
|
for (const g of KAMERA_GROESSEN) {
|
|
const a = await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.admin, rumpf: { kamera_gross: g },
|
|
});
|
|
ok(a.code === 200 && a.j?.kamera_gross === g, `Groesse ${g} (${a.code})`);
|
|
}
|
|
for (const e of KAMERA_ECKEN) {
|
|
const a = await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.admin, rumpf: { kamera_ecke: e },
|
|
});
|
|
ok(a.code === 200 && a.j?.kamera_ecke === e, `Ecke ${e} (${a.code})`);
|
|
}
|
|
|
|
/* ---- UND WAS NICHT IN DER LISTE STEHT, WIRD ABGELEHNT ----
|
|
Die Gegenprobe zu oben. Eine Pruefung, die nur bestaetigt, dass
|
|
das Erlaubte geht, bestaetigt nichts ueber die Grenze. */
|
|
const zuGross = await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.admin, rumpf: { kamera_gross: 8 },
|
|
});
|
|
ok(zuGross.code === 400 && zuGross.j?.fehler === "kameragroesse_unklar",
|
|
`achtfach wird abgelehnt (${zuGross.code}/${zuGross.j?.fehler})`);
|
|
const quatschE = await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.admin, rumpf: { kamera_ecke: "mitte" },
|
|
});
|
|
ok(quatschE.code === 400 && quatschE.j?.fehler === "ecke_unklar",
|
|
`eine erfundene Ecke wird abgelehnt (${quatschE.code}/${quatschE.j?.fehler})`);
|
|
const leer = await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.admin, rumpf: {},
|
|
});
|
|
ok(leer.code === 400 && leer.j?.fehler === "nichts_zu_tun",
|
|
`ein Aufruf ohne Inhalt loest keinen Rundruf aus (${leer.code})`);
|
|
const fremd = await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.modi, rumpf: { kamera_gross: 2 },
|
|
});
|
|
ok(fremd.code === 403, `ein Modi stellt die Groesse nicht (${fremd.code})`);
|
|
|
|
/* ---- DREI EINSTELLUNGEN IN EINEM AUFRUF ----
|
|
Das ist der Grund, warum es EIN Weg ist. Drei getrennte Aufrufe
|
|
waeren drei Rundrufe, und zwischen ihnen saehen die Zusehenden
|
|
eine Mischung: neue Anordnung, alte Ecke. */
|
|
const alles = await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.admin,
|
|
rumpf: { layout: "gleich", kamera_gross: 1.5, kamera_ecke: "ol" },
|
|
});
|
|
ok(alles.code === 200 && alles.j?.layout === "gleich"
|
|
&& alles.j?.kamera_gross === 1.5 && alles.j?.kamera_ecke === "ol",
|
|
`Anordnung, Groesse und Ecke in einem Zug (${alles.code})`);
|
|
/* UND EINE FALSCHE ANGABE AENDERT GAR NICHTS. Sonst waere eine
|
|
abgelehnte Ecke eine halb durchgefuehrte Umstellung -- die
|
|
Anordnung stuende neu, die Ecke alt, und die Fehlermeldung
|
|
saehe aus, als sei nichts passiert. */
|
|
const halb = await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.admin,
|
|
rumpf: { layout: "kino", kamera_ecke: "diagonal" },
|
|
});
|
|
const danach = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
|
|
ok(halb.code === 400 && danach.j?.layout === "gleich",
|
|
`eine falsche Angabe laesst auch das Gute stehen (${danach.j?.layout})`);
|
|
|
|
/* ---- DER STAND TRAEGT BEIDES UND DIE AUSWAHL DAZU ----
|
|
Die Regie baut ihre Knoepfe daraus. Baute sie sie aus einer
|
|
eigenen Liste, waere das eine zweite Liste, die irgendwann von
|
|
der ersten abweicht -- und dann steht ein Knopf da, den der
|
|
Server ablehnt. */
|
|
await roh(CREW, "/workspace/api/reaktion/layout", {
|
|
method: "POST", keks: k.admin,
|
|
rumpf: { layout: "kino", kamera_gross: 1, kamera_ecke: "ur" },
|
|
});
|
|
const stand = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
|
|
ok(stand.j?.kamera_gross === 1 && stand.j?.kamera_ecke === "ur",
|
|
"der Stand traegt Groesse und Ecke");
|
|
ok(Array.isArray(stand.j?.kamera_groessen)
|
|
&& stand.j.kamera_groessen.length === KAMERA_GROESSEN.length
|
|
&& Array.isArray(stand.j?.kamera_ecken)
|
|
&& stand.j.kamera_ecken.length === 4,
|
|
"und die Auswahl kommt vom Server, nicht aus dem Browser");
|
|
|
|
/* ---- AUCH DIE ZUSEHENDEN BEKOMMEN ES ----
|
|
Es ist Teil des Bildes, nicht der Regie. Wer es nicht bekaeme,
|
|
saehe die Fenster woanders als der, der darueber redet. */
|
|
const gast = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast });
|
|
ok(gast.j?.kamera_gross === 1 && gast.j?.kamera_ecke === "ur",
|
|
"auch ein Zuschauer bekommt Groesse und Ecke");
|
|
|
|
/* ---- WAS DER QUELLTEXT HERGIBT ---------------------------------- */
|
|
{
|
|
const css = readFileSync(join(HIER, "..", "workspace", "assets", "css", "reaktion.css"), "utf8");
|
|
const js = readFileSync(join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8");
|
|
const mld = readFileSync(join(HIER, "..", "workspace", "assets", "js", "meldung.js"), "utf8");
|
|
|
|
/* ==== KOMMENTARE RAUS, BEVOR IRGENDETWAS GEZAEHLT WIRD ========
|
|
Beim ersten Lauf meldete die Pruefung „1x steht eine feste
|
|
Breite am Fenster" -- und fand sie in einem Kommentar, der
|
|
genau davor warnt und die entfernte Zeile zitiert. Ein
|
|
Werkzeug, das bei jedem Lauf meckert, wird nach dem zweiten
|
|
Mal weggeklickt, zusammen mit dem echten Befund darin.
|
|
Gezaehlt werden Regeln, nicht Prosa. */
|
|
const regeln = css.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
|
|
/* JEDE ECKE BRAUCHT EINE REGEL -- sonst ist sie ein Knopf, der
|
|
nichts tut. */
|
|
const ohneEcke = KAMERA_ECKEN.filter((e) => !regeln.includes(`[data-ecke="${e}"]`));
|
|
ok(ohneEcke.length === 0,
|
|
ohneEcke.length ? `OHNE REGEL: ${ohneEcke.join(", ")}` : "jede Ecke hat eine eigene Regel");
|
|
ok(!regeln.includes('[data-ecke="mitte"]'),
|
|
"Gegenprobe: eine erfundene Ecke hat keine");
|
|
|
|
/* ---- EINE BREITE, EINE STELLE ----
|
|
Bis zum 28.09. stand die Breite eines Kamerafensters zweimal im
|
|
Stilblatt: als Grundregel und noch einmal fuer „Kameras gross".
|
|
Wer sie einstellbar macht, muesste beide mitdenken, und die
|
|
zweite wird vergessen. Das ist im selben Haus am selben Tag
|
|
zweimal passiert (die Saalzeilen, die Tafeln). */
|
|
const breiten = [...regeln.matchAll(/\.kamera\s*\{[^}]*?\bwidth:\s*clamp\(/g)];
|
|
ok(breiten.length === 0,
|
|
breiten.length
|
|
? `${breiten.length}x steht eine feste Breite am Fenster statt an --kam`
|
|
: "die Breite eines Fensters steht nur als --kam da");
|
|
ok(/--kam:\s*calc\([\s\S]*?var\(--kam-gross\)/.test(regeln),
|
|
"und der Faktor der Regie geht dort ein");
|
|
/* UND GERECHNET WIRD NUR EINMAL. `--kam-grund` darf jede
|
|
Anordnung und jede Bildschirmbreite fuer sich setzen -- die
|
|
Rechnung daraus steht an einer Stelle. Wer sie ein zweites Mal
|
|
hinschreibt, verliert den Faktor der Regie genau dort, wo er
|
|
es nicht sieht. Am 28.09. war das der Handy-Block. */
|
|
const rechnungen = [...regeln.matchAll(/--kam:\s*/g)];
|
|
ok(rechnungen.length === 1,
|
|
`die Breite wird ${rechnungen.length}x gerechnet (soll: 1x)`);
|
|
|
|
/* ---- EIN DECKEL, DER NICHT VOM FAKTOR ABHAENGT ----
|
|
Bei zweifacher Groesse auf einem schmalen Bildschirm deckte ein
|
|
einzelnes Fenster sonst die halbe Leinwand zu. Die Messung
|
|
nebenan prueft das am Bild; hier steht, dass es ueberhaupt
|
|
einen Deckel gibt. */
|
|
const stapel = regeln.match(/\.kameras \{[^}]*\}/);
|
|
ok(!!stapel && /max-width:\s*\d+%/.test(stapel[0]),
|
|
"der Stapel hat einen Deckel in Prozent der Leinwand");
|
|
|
|
/* ---- DIE REGIE BAUT AUS DER SERVERLISTE ---- */
|
|
ok(/lage\.kamera_groessen/.test(js) && /lage\.kamera_ecken/.test(js),
|
|
"die Regie baut ihre Knoepfe aus der Liste des Servers");
|
|
ok(!/\[\s*0\.6\s*,\s*0\.8\s*,\s*1\s*,/.test(js),
|
|
"Gegenprobe: sie haelt keine zweite Liste vor");
|
|
|
|
/* ---- EIN KNOPF, DER NICHTS TUT, WIRD NICHT ANGEBOTEN ----
|
|
Die Ecke wirkt nur bei „Video gross"; sonst stehen die Fenster
|
|
mittig oder fuellen die Flaeche. */
|
|
ok(/k\.disabled = !eckeGilt/.test(js),
|
|
"ausserhalb von „Video gross“ ist die Ecke nicht zu stellen");
|
|
/* UND DIE GROESSE BEI „NUR VIDEO“: Dort gibt es gar keine
|
|
Kamerafenster. Wer dann auf 2x drueckt, sieht nichts passieren
|
|
und sucht den Fehler bei sich. */
|
|
ok(/k\.disabled = !kamerasDa/.test(js),
|
|
"und bei „Nur Video“ ist auch die Groesse nicht zu stellen");
|
|
ok(/kamecke-hinweis/.test(js), "und die Regie sagt auch, warum");
|
|
|
|
/* ---- DER RUNDRUF BRINGT DAS GANZE BILD ----
|
|
Sonst saehen die Zusehenden die neue Anordnung mit der alten
|
|
Ecke, bis zufaellig etwas anderes den ganzen Stand nachzieht. */
|
|
ok(/was === 'layout'/.test(js) && /d\.kamera_gross/.test(js) && /d\.kamera_ecke/.test(js),
|
|
"die Meldung „layout“ bringt Anordnung, Groesse und Ecke");
|
|
|
|
/* ---- UND DIE ABLEHNUNG HAT EINEN DEUTSCHEN SATZ ----
|
|
Ein Fehlerkuerzel auf dem Bildschirm ist keine Antwort. */
|
|
for (const wort of ["kameragroesse_unklar", "ecke_unklar", "nichts_zu_tun"]) {
|
|
ok(mld.includes(wort), `„${wort}" hat einen deutschen Satz`);
|
|
}
|
|
}
|
|
}
|
|
|
|
melde("");
|
|
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
|
|
process.exit(fehler === 0 ? 0 : 1);
|