/* ===================================================================== WIE DAS FOYER AUSSIEHT (08.10.2026) `pruef-reaktion` misst, was die Schnittstelle antwortet. Das sagt nichts darueber, ob die drei Reihen auf einem Bildschirm nebeneinander passen, ob der Knopf in der Mitte ins Auge fiele und ob am Telefon die Sendung oben steht. WARUM EIGENES WERKZEUG UND KEIN ABSCHNITT IN mess-reaktion: Dort laufen Kameras, Video und Chat -- jede geoeffnete Seite haelt zwei stehende Verbindungen, und der Browser gibt je Adresse nur wenige her. Das Foyer braucht keine davon. Es misst in Sekunden, was dort zwischen zwei Kameraaufbauten untergehen wuerde. GEMESSEN WIRD, WAS MAN NICHT ERRATEN KANN: * Steht der Plan bei einem Gast wirklich NICHT da -- und steht auch kein Stueck daraus irgendwo im Dokument? * Ist die Mitte breiter als die Seiten, und rutschen die zwei uebrigen Reihen auf, wenn der Plan fehlt? * Steht am Telefon die Sendung in der ERSTEN Zeile? * Ist der Knopf bei "zu" weg und bei "gleich"/"live" da? * Sind alle Beruehrziele 44 Pixel hoch und alle Schriften mindestens 11,5 Pixel gross? * Und geht der ganze Weg durch: Gast schickt ein Video, DogFather sieht es MIT Namen, nimmt es -- und es steht links im Plan. ZU JEDER MESSUNG, DIE "in Ordnung" SAGEN KANN, GEHOERT EINE GEGENPROBE. Am Ende wird jede der tragenden Messungen absichtlich kaputtgemacht und muss anschlagen. Eine Messung, die immer bestaetigt, bestaetigt nichts. NIE DIE ECHTE DATENBANK. Eigener Ordner, eigener Port, eigenes Zertifikat -- und am Ende ist alles weg. DREI AUSGAENGE: 0 in Ordnung 1 ein Befund 3 konnte nicht nachsehen (Playwright fehlt, kein Zertifikat, die Buehne nimmt nichts an, oder der Stand hat sich waehrend des Laufs geaendert) Aufrufen mit: node server/mess-foyer.mjs ===================================================================== */ import { mkdtempSync, rmSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { request } from "node:http"; import { createServer as httpsServer } from "node:https"; import { zertifikatBauen } from "./helfer-openssl.mjs"; import { notbremse } from "./helfer-notbremse.mjs"; import { eigenerPort } from "./helfer-port.mjs"; const HIER = dirname(fileURLToPath(import.meta.url)); const ordner = mkdtempSync(join(tmpdir(), "mess-foyer-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); const PORT = await eigenerPort(import.meta); const HTTPS_PORT = await eigenerPort(import.meta, "mess-foyer (HTTPS)", 1); process.env.PORT = `${PORT}`; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; const express = (await import("express")).default; /* Der Sitzungskeks ist `secure`. Der Vorbau unten spricht https, der Server dahinter nicht -- ohne diesen Griff setzt er den Keks und der Browser verwirft ihn. */ 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(240_000, "mess-foyer"); await new Promise((r) => setTimeout(r, 1000)); const CREW = "crew.dogfather-universe.com"; /* ---------- Leute ------------------------------------------------------- */ const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const kenn = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") .run("code_kennung_schluessel", kenn, new Date().toISOString()); const ALT = new Date(Date.now() - 200 * 86400000).toISOString(); function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n," + " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, createHmac("sha256", kenn).update(code).digest("hex"), ALT); } const CODES = { admin: "CODE-M00-000001", hand: "CODE-M01-000001", gast: "CODE-M02-000001", gast2: "CODE-M03-000001", }; /* VANVAN IST DIE RECHTE HAND und wird mitgemessen, nicht nur DogFather: Filipe hat "nur ich und vanvan" gesagt, und ob die zweite Haelfte davon stimmt, sieht man nur, wenn sie dabei ist. */ anlegen("DogFather", "admin", CODES.admin); anlegen("VanVan", "hand", CODES.hand); anlegen("Nachtfalter", "gast", CODES.gast); anlegen("Lenny", "gast", CODES.gast2); d.close(); function roh(pfad, { method = "GET", keks = "", rumpf = null } = {}) { return new Promise((fertig, schief) => { const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf)); const a = request({ host: "127.0.0.1", port: PORT, path: pfad, method, headers: { Host: CREW, Origin: `https://${CREW}`, ...(keks ? { Cookie: keks } : {}), ...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}), }, }, (antwort) => { const teile = []; antwort.on("data", (s) => teile.push(s)); antwort.on("end", () => { const text = Buffer.concat(teile).toString("utf8"); let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ } fertig({ code: antwort.statusCode, text, j, kopf: antwort.headers }); }); }); a.on("error", schief); if (daten) a.write(daten); a.end(); }); } async function anmelden(wer) { const a = await roh("/workspace/api/anmelden", { method: "POST", rumpf: { rolle: wer === "gast2" ? "gast" : wer, code: CODES[wer], alter_ok: true }, }); return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); } const k = { admin: await anmelden("admin"), hand: await anmelden("hand"), gast: await anmelden("gast"), gast2: await anmelden("gast2"), }; /* ---------- Zaehlwerk --------------------------------------------------- */ let fehler = 0; let geprueft = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; function nichtNachsehbar(grund) { console.log("\n -- NICHT NACHSEHBAR"); console.log(` ${grund}`); console.log(" Das ist KEIN Befund am Code -- es heisst nur, dass"); console.log(" gerade niemand nachsehen konnte.\n"); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(3); } /* ---------- Die Buehne fuellen ------------------------------------------ */ /* ZWEI STUECKE IN DER WARTESCHLANGE, EINES MIT TERMIN UND EINES OHNE. Genau das ist die Aussage der linken Reihe ("was keinen hat, steht unten"), und mit nur einem Eintrag liesse sie sich nicht messen. */ for (const adresse of ["https://youtu.be/9bZkp7q19f0", "https://www.youtube.com/watch?v=kJQP7kiw5Fk"]) { const a = await roh("/workspace/api/reaktion/liste", { method: "POST", keks: k.admin, rumpf: { video: adresse }, }); if (a.code >= 400) nichtNachsehbar(`Die Warteschlange nimmt nichts an (HTTP ${a.code}).`); } const liste0 = (await roh("/workspace/api/reaktion", { keks: k.admin })).j?.liste || []; if (liste0.length !== 2) { nichtNachsehbar(`In der Warteschlange stehen ${liste0.length} Stuecke statt 2.`); } /* Der Termin liegt in der ZUKUNFT und wird relativ gerechnet. Ein festes Datum waere eine Zeitbombe: Es ist richtig, bis der Kalender es einholt -- und dann schlaegt "ueberfaellig" an, ohne dass etwas kaputt ist. */ { const a = await roh(`/workspace/api/reaktion/liste/${liste0[0].id}/wann`, { method: "PUT", keks: k.admin, rumpf: { wann: new Date(Date.now() + 3 * 86400000).toISOString() }, }); if (a.code >= 400) nichtNachsehbar(`Der Termin laesst sich nicht setzen (HTTP ${a.code}).`); } await roh("/workspace/api/reaktion", { method: "PUT", keks: k.admin, rumpf: { titel: "Wir schauen den neuen Trailer", video: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", beginnt_am: new Date(Date.now() + 7 * 60_000).toISOString(), }, }); /* ---------- Der Vorbau -------------------------------------------------- */ const schluesselDatei = join(ordner, "p.key"); const zertDatei = join(ordner, "p.crt"); try { zertifikatBauen(schluesselDatei, zertDatei, CREW); } catch (f) { nichtNachsehbar(`Kein Zertifikat zu bauen: ${f?.message}\n` + " openssl liegt bei Git in mingw64/bin -- im System-PATH\n" + " steht oft nur Git/cmd."); } const vorbau = httpsServer({ key: readFileSync(schluesselDatei), cert: readFileSync(zertDatei), }, (von, nach) => { const weiter = request({ host: "127.0.0.1", port: PORT, path: von.url, method: von.method, headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` }, }, (antwort) => { nach.writeHead(antwort.statusCode || 502, antwort.headers); antwort.pipe(nach); }); weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } }); von.pipe(weiter); }); await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r)); const BASIS = `https://${CREW}:${HTTPS_PORT}`; let pw = null; try { pw = await import( "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); } catch (f) { nichtNachsehbar(`Playwright ist nicht da: ${f?.message}`); } const browser = await pw.chromium.launch({ args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"], }); async function alsWer(wer, breite, hoehe) { const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe }, /* MIT FINGER, WENN ES EIN HANDY IST. Ohne `hasTouch` meldet der Browser `pointer: fine` -- und damit greift KEINE Regel aus `@media (pointer: coarse)`. Dort stehen im ganzen Haus die 44-Pixel-Beruehrziele. Eine Handy-Messung mit Mauszeiger misst eine Seite, die auf keinem Handy so aussieht. */ hasTouch: breite <= 860, ignoreHTTPSErrors: true, }); await kontext.addCookies((k[wer] || "").split("; ").filter(Boolean).map((z) => { const [n, ...r] = z.split("="); return { name: n, value: r.join("="), domain: CREW, path: "/" }; })); const seite = await kontext.newPage(); const konsole = []; seite.on("pageerror", (f) => konsole.push(String(f?.message || f))); seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); seite.on("response", (a) => { /* Vorschaubilder kommen von YouTube und sind hier nicht erreichbar -- das ist die Umgebung und kein Befund. */ if (a.status() >= 400 && !/ytimg\.com/.test(a.url())) { konsole.push(`${a.status()} ${a.url().replace(/^https?:\/\/[^/]+/, "")}`); } }); await seite.goto(`${BASIS}/workspace/reaktion-foyer.html`, { waitUntil: "networkidle" }); /* NICHT AUF EINE ZEIT WARTEN, SONDERN AUF DIE AUSKUNFT. Solange im Kopf "Wird geladen" steht, ist der Stand noch nicht da -- eine Messung davor misst den leeren Rahmen. */ await seite.waitForFunction( () => !/Wird geladen/.test(document.getElementById("kopf-satz")?.textContent || ""), null, { timeout: 15000 }); await seite.waitForTimeout(250); return { kontext, seite, konsole }; } async function bild(seite, name) { await seite.screenshot({ path: join(HIER, `mess-foyer-${name}.png`), fullPage: true }); console.log(` Bild: server/mess-foyer-${name}.png`); } /* ===================================================================== DIE MESSUNGEN, ALLE IM BROWSER Sie stehen als Funktionen da und nicht als Code im Ablauf: Nur so kann die Gegenprobe am Ende DIESELBE Messung auf eine absichtlich kaputte Seite anwenden. Zwei Abschriften derselben Frage waeren zwei Antworten, und eine davon waere irgendwann aelter. ===================================================================== */ /** Welche Reihen stehen da, wo, wie breit -- und wie viele Spalten? */ const reihenLesen = (seite) => seite.evaluate(() => { const da = (id) => { const e = document.getElementById(id); if (!e) return null; /* `hidden` allein genuegt nicht: Eine Reihe kann auch durch `display:none` am Stilblatt verschwinden. Gefragt ist, ob ein Mensch sie sieht. */ if (!e.offsetParent) return null; const r = e.getBoundingClientRect(); return { oben: Math.round(r.top), breit: Math.round(r.width), hoch: Math.round(r.height) }; }; return { plan: da("reihe-plan"), saal: da("reihe-saal"), raten: da("reihe-raten"), spalten: getComputedStyle(document.getElementById("reihen")) .gridTemplateColumns.split(/\s+/).filter(Boolean).length, }; }); /** Steht eines dieser Woerter irgendwo im Dokument? */ const leckLesen = (seite, woerter) => seite.evaluate((w) => { const text = document.body.innerText + " " + document.body.innerHTML; return w.filter((z) => z && text.includes(z)); }, woerter); /** Das kleinste Beruehrziel, die kleinste Schrift, der Ueberstand. */ const massLesen = (seite) => seite.evaluate(() => { const wurzel = document.getElementById("foyer"); const sichtbar = (e) => { const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0 && e.offsetParent !== null; }; let ziel = null; for (const e of wurzel.querySelectorAll("a, button, input, select, textarea")) { if (!sichtbar(e)) continue; const r = e.getBoundingClientRect(); if (!ziel || Math.round(r.height) < ziel.hoch) { ziel = { hoch: Math.round(r.height), breit: Math.round(r.width), was: e.id || e.className || e.tagName, }; } } let schrift = null; for (const e of wurzel.querySelectorAll("*")) { if (!sichtbar(e)) continue; /* NUR ELEMENTE, DIE SELBST TEXT TRAGEN. Sonst waere jede Umhuellung mitgezaehlt, und die Messung sagte etwas ueber den Kasten statt ueber die Schrift. */ if (![...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())) continue; const gr = parseFloat(getComputedStyle(e).fontSize); if (!schrift || gr < schrift.gr) schrift = { gr, was: e.id || e.className || e.tagName }; } return { ziel, schrift, ueber: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth), }; }); /** Was steht in der Mitte? */ const mitteLesen = (seite) => seite.evaluate(() => { const t = (id) => document.getElementById(id)?.textContent.trim() || ""; const knopf = document.getElementById("saal-knopf"); return { lampe: t("lampe-wort"), stand: document.getElementById("lampe")?.dataset.stand || "?", titel: t("saal-titel"), satz: t("saal-satz"), knopfDa: !!(knopf && knopf.offsetParent), knopfWort: t("saal-knopf-wort"), knopfZiel: knopf?.getAttribute("href") || "", }; }); /** Die Zeilen der linken Reihe. */ const planLesen = (seite) => seite.evaluate(() => [ ...document.querySelectorAll("#plan-liste .plan-zeile")].map((z) => ({ titel: z.querySelector(".plan-zeile__titel")?.textContent.trim() || "", wann: z.querySelector(".plan-zeile__wann")?.textContent.trim() || "", termin: z.dataset.termin || "", }))); /** Die Zeilen der rechten Reihe. */ const ratenLesen = (seite) => seite.evaluate(() => [ ...document.querySelectorAll("#raten-liste .raten-zeile")].map((z) => ({ titel: z.querySelector(".raten-zeile__titel")?.textContent.trim() || "", unter: z.querySelector(".raten-zeile__unter")?.textContent.trim() || "", stand: z.dataset.stand || "", knoepfe: [...z.querySelectorAll(".raten-knopf")].map((b) => b.textContent.trim()), }))); /* ===================================================================== 1. GESCHLOSSEN, AM RECHNER, ALS COMMUNITY ===================================================================== */ console.log("\n=== 1. Geschlossen, am Rechner, als Community ==="); { const gast = await alsWer("gast", 1440, 900); const r = await reihenLesen(gast.seite); ok(r.plan === null, "Gast: die linke Reihe steht gar nicht da"); ok(r.saal !== null && r.raten !== null, "Gast: Mitte und Anraten stehen da"); ok(r.spalten === 2, `Gast: zwei Spalten, nicht drei (${r.spalten})`); ok(!!(r.saal && r.raten) && r.saal.breit > r.raten.breit, `Gast: die Mitte ist die breitere (${r.saal?.breit} zu ${r.raten?.breit} px)`); const leck = await leckLesen(gast.seite, [...liste0.map((z) => z.titel), ...liste0.map((z) => z.video)]); ok(leck.length === 0, "Gast: kein Stueck aus der Warteschlange im Dokument" + (leck.length ? " -> " + leck.join(", ") : "")); const m = await mitteLesen(gast.seite); ok(m.lampe === "Geschlossen", `Gast: die Lampe sagt Geschlossen (${m.lampe})`); ok(m.knopfDa === false, "Gast: bei zu gibt es keinen Knopf"); ok(m.satz.length > 20, `Gast: ein Satz erklaert den Zustand (${m.satz.length} Zeichen)`); const mass = await massLesen(gast.seite); ok(mass.ueber === 0, `Gast: nichts ragt seitlich hinaus (${mass.ueber} px)`); ok(!!mass.schrift && mass.schrift.gr >= 11.5, `Gast: kleinste Schrift ${mass.schrift?.gr} px (noetig 11,5) bei ${mass.schrift?.was}`); ok(gast.konsole.length === 0, "Gast: nichts in der Konsole" + (gast.konsole.length ? " -> " + gast.konsole.slice(0, 3).join(" | ") : "")); await bild(gast.seite, "1-zu-gast"); await gast.kontext.close(); } /* ===================================================================== 2. GESCHLOSSEN, DOGFATHER UND VANVAN ===================================================================== */ console.log("\n=== 2. Geschlossen, DogFather und VanVan ==="); for (const wer of ["admin", "hand"]) { const chef = await alsWer(wer, 1440, 900); const r = await reihenLesen(chef.seite); ok(r.plan !== null, `${wer}: die linke Reihe steht da`); ok(r.spalten === 3, `${wer}: drei Spalten (${r.spalten})`); ok(!!(r.plan && r.saal && r.raten) && r.saal.breit > r.plan.breit && r.saal.breit > r.raten.breit, `${wer}: die Mitte ist die breiteste ` + `(${r.plan?.breit} / ${r.saal?.breit} / ${r.raten?.breit} px)`); const p = await planLesen(chef.seite); ok(p.length === 2, `${wer}: beide Stuecke stehen im Plan (${p.length})`); ok(p[0]?.termin === "ja", `${wer}: das mit Termin steht oben (${p[0]?.termin || "ohne"})`); ok(/\d{1,2}:\d{2}/.test(p[0]?.wann || ""), `${wer}: beim ersten steht eine Uhrzeit (${p[0]?.wann})`); ok(p[1]?.wann === "noch ohne Termin", `${wer}: das ohne Termin steht unten und sagt das (${p[1]?.wann})`); const mass = await massLesen(chef.seite); ok(mass.ueber === 0, `${wer}: nichts ragt seitlich hinaus (${mass.ueber} px)`); ok(chef.konsole.length === 0, `${wer}: nichts in der Konsole` + (chef.konsole.length ? " -> " + chef.konsole.slice(0, 3).join(" | ") : "")); if (wer === "admin") await bild(chef.seite, "2-zu-dogfather"); await chef.kontext.close(); } /* ===================================================================== 3. GLEICH -- DER WARTEBEREICH IST OFFEN ===================================================================== */ console.log("\n=== 3. Gleich (Wartebereich) ==="); await roh("/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "vorbereitung" } }); { const gast = await alsWer("gast", 1440, 900); const m = await mitteLesen(gast.seite); ok(m.lampe === "Gleich", `die Lampe sagt Gleich (${m.lampe})`); ok(m.knopfDa, "der Knopf steht da -- auch bei der Vorschau schon"); ok(m.knopfWort === "In den Wartebereich", `er heisst In den Wartebereich (${m.knopfWort})`); ok(m.knopfZiel === "reaktion.html", `er fuehrt in den Saal (${m.knopfZiel})`); ok(/Start um/.test(m.satz), `der Satz nennt die Startzeit (${m.satz})`); ok(m.titel.includes("Trailer"), `der Titel der Sendung steht da (${m.titel})`); await bild(gast.seite, "3-gleich-gast"); await gast.kontext.close(); } /* ===================================================================== 4. AUF SENDUNG ===================================================================== */ console.log("\n=== 4. Auf Sendung ==="); await roh("/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "live" } }); { const gast = await alsWer("gast", 1440, 900); const m = await mitteLesen(gast.seite); ok(m.lampe === "Auf Sendung", `die Lampe sagt Auf Sendung (${m.lampe})`); ok(m.knopfWort === "Jetzt beitreten", `der Knopf heisst Jetzt beitreten (${m.knopfWort})`); ok(/dieselbe Stelle/.test(m.satz), `der Satz verspricht den Gleichlauf (${m.satz})`); await bild(gast.seite, "4-live-gast"); await gast.kontext.close(); } /* ===================================================================== 5. AM TELEFON -- DIE SENDUNG STEHT OBEN ===================================================================== */ console.log("\n=== 5. Am Telefon (390 px) ==="); { const gast = await alsWer("gast", 390, 844); const r = await reihenLesen(gast.seite); ok(r.plan === null, "Gast am Telefon: keine linke Reihe"); ok(!!(r.saal && r.raten) && r.saal.oben < r.raten.oben, `Gast am Telefon: die Sendung steht ueber dem Anraten ` + `(${r.saal?.oben} vor ${r.raten?.oben} px)`); const mass = await massLesen(gast.seite); ok(mass.ueber === 0, `Gast am Telefon: nichts ragt seitlich hinaus (${mass.ueber} px)`); ok(!!mass.ziel && mass.ziel.hoch >= 44, `Gast am Telefon: kleinstes Beruehrziel ${mass.ziel?.hoch} px ` + `(noetig 44) bei ${mass.ziel?.was}`); ok(!!mass.schrift && mass.schrift.gr >= 11.5, `Gast am Telefon: kleinste Schrift ${mass.schrift?.gr} px bei ${mass.schrift?.was}`); await bild(gast.seite, "5-live-handy-gast"); await gast.kontext.close(); } { const chef = await alsWer("admin", 390, 844); const r = await reihenLesen(chef.seite); ok(!!(r.plan && r.saal && r.raten) && r.saal.oben < r.raten.oben && r.raten.oben < r.plan.oben, "DogFather am Telefon: Sendung, Anraten, Plan -- in dieser Folge " + `(${r.saal?.oben} / ${r.raten?.oben} / ${r.plan?.oben} px)`); ok(r.spalten === 1, `DogFather am Telefon: eine Spalte (${r.spalten})`); const mass = await massLesen(chef.seite); ok(mass.ueber === 0, `DogFather am Telefon: nichts ragt hinaus (${mass.ueber} px)`); ok(!!mass.ziel && mass.ziel.hoch >= 44, `DogFather am Telefon: kleinstes Beruehrziel ${mass.ziel?.hoch} px bei ${mass.ziel?.was}`); await bild(chef.seite, "6-live-handy-dogfather"); await chef.kontext.close(); } /* ===================================================================== 6. DER GANZE WEG: ANRATEN, SEHEN, NEHMEN Hier wird nicht die Schnittstelle gefragt, sondern GEKLICKT. Der Unterschied ist nicht theoretisch: Ein Knopf, an dem nichts haengt, antwortet in der Pruefung nebenan tadellos. ===================================================================== */ console.log("\n=== 6. Anraten, sehen, nehmen ==="); { const gast = await alsWer("gast", 1440, 900); await gast.seite.fill("#raten-video", "https://www.youtube.com/watch?v=aqz-KE-bpKQ"); await gast.seite.fill("#raten-notiz", "Das muessen wir zusammen sehen."); await gast.seite.click("#raten-los"); await gast.seite.waitForFunction( () => document.querySelectorAll("#raten-liste .raten-zeile").length === 1, null, { timeout: 10000 }); const meine = await ratenLesen(gast.seite); ok(meine.length === 1, `der Gast sieht seinen eigenen Vorschlag (${meine.length})`); ok(meine[0]?.stand === "offen", `er steht auf offen (${meine[0]?.stand})`); ok(meine[0]?.unter === "wartet", `und sagt wartet -- ohne Namen, auch nicht den eigenen (${meine[0]?.unter})`); ok(!meine[0]?.knoepfe.includes("Nehmen"), "der Gast hat keinen Nehmen-Knopf"); ok(meine[0]?.knoepfe.includes("\u00d7"), "aber er darf zurueckziehen, was noch offen ist"); const fuss = await gast.seite.textContent("#raten-fuss"); ok(/noch 4 Videos/.test(fuss || ""), `er erfaehrt, wie viele noch gehen (${fuss})`); await bild(gast.seite, "7-angeraten-gast"); await gast.kontext.close(); } { const chef = await alsWer("admin", 1440, 900); const alle = await ratenLesen(chef.seite); ok(alle.length === 1, `DogFather sieht den Vorschlag (${alle.length})`); ok(/Nachtfalter/.test(alle[0]?.unter || ""), `und bei ihm steht, wer es war (${alle[0]?.unter})`); ok(alle[0]?.knoepfe.includes("Nehmen") && alle[0]?.knoepfe.includes("Diesmal nicht"), `er hat beide Knoepfe (${alle[0]?.knoepfe.join(" / ")})`); const zahl = await chef.seite.textContent("#raten-zahl"); ok(/1 offen/.test(zahl || ""), `das Schild zaehlt die offenen (${zahl})`); await bild(chef.seite, "8-angeraten-dogfather"); /* NEHMEN -- und links muss es ankommen. */ await chef.seite.click(".raten-knopf--ja"); await chef.seite.waitForFunction( () => document.querySelectorAll("#plan-liste .plan-zeile").length === 3, null, { timeout: 10000 }); const p = await planLesen(chef.seite); ok(p.length === 3, `nach Nehmen stehen drei Stuecke im Plan (${p.length})`); const danach = await ratenLesen(chef.seite); ok(danach[0]?.stand === "uebernommen", `der Vorschlag steht auf uebernommen (${danach[0]?.stand})`); ok(/kommt dran/.test(danach[0]?.unter || ""), `und sagt kommt dran (${danach[0]?.unter})`); await bild(chef.seite, "9-genommen-dogfather"); await chef.kontext.close(); } { /* UND DER EINSENDER ERFAEHRT ES. Ohne diese Messung waere der Stand nur im Pult richtig und beim Gast von gestern. */ const gast = await alsWer("gast", 1440, 900); const meine = await ratenLesen(gast.seite); ok(meine[0]?.stand === "uebernommen", `der Gast sieht den neuen Stand (${meine[0]?.stand})`); ok(!meine[0]?.knoepfe.includes("\u00d7"), "und kann nichts mehr zurueckziehen, was entschieden ist"); await gast.kontext.close(); } { /* LENNY SIEHT NICHTS DAVON -- "nur ich und vanvan sehen sie", gemessen und nicht nur gelesen. */ const lenny = await alsWer("gast2", 1440, 900); const seine = await ratenLesen(lenny.seite); ok(seine.length === 0, `Lenny sieht fremde Vorschlaege nicht (${seine.length})`); const leck = await leckLesen(lenny.seite, ["Nachtfalter", "aqz-KE-bpKQ"]); ok(leck.length === 0, "und auch nicht im Dokument" + (leck.length ? " -> " + leck.join(", ") : "")); await lenny.kontext.close(); } /* ===================================================================== 7. DIE GEGENPROBEN Fuenf Messungen oben koennen "in Ordnung" sagen. Hier wird jede absichtlich kaputtgemacht -- schlaegt sie nicht an, ist sie wertlos, und alle gruenen Haken davor beweisen nichts. ===================================================================== */ console.log("\n=== 7. Gegenproben ==="); { const chef = await alsWer("admin", 390, 844); /* (a) Die Reihenfolge am Telefon. */ await chef.seite.addStyleTag({ content: ".foyer-reihe--saal { order: 9 !important; }" }); await chef.seite.waitForTimeout(120); const verdreht = await reihenLesen(chef.seite); ok(!(verdreht.saal.oben < verdreht.raten.oben), `Gegenprobe: eine verdrehte Reihenfolge wird erkannt ` + `(${verdreht.saal.oben} / ${verdreht.raten.oben} px)`); /* (b) Ein zu kleines Beruehrziel. */ await chef.seite.addStyleTag({ content: "#raten-los { min-height: 0 !important; height: 20px !important;" + " padding: 0 !important; }", }); await chef.seite.waitForTimeout(120); const klein = await massLesen(chef.seite); ok(!!klein.ziel && klein.ziel.hoch < 44, `Gegenprobe: ein 20-px-Knopf wird gefunden (${klein.ziel?.hoch} px)`); /* (c) Eine zu kleine Schrift. */ await chef.seite.addStyleTag({ content: "#saal-satz { font-size: 9px !important; }" }); await chef.seite.waitForTimeout(120); const winzig = await massLesen(chef.seite); ok(!!winzig.schrift && winzig.schrift.gr < 11.5, `Gegenprobe: 9-px-Schrift wird gefunden (${winzig.schrift?.gr} px)`); /* (d) Und ein Ueberstand. */ await chef.seite.addStyleTag({ content: "#saal-titel { width: 900px !important; }" }); await chef.seite.waitForTimeout(120); const breit = await massLesen(chef.seite); ok(breit.ueber > 0, `Gegenprobe: ein Ueberstand wird gemessen (${breit.ueber} px)`); await chef.kontext.close(); } { const gast = await alsWer("gast", 1440, 900); /* (e) Das Leck. Ein Wort wird von Hand ins Dokument geschrieben -- findet die Messung es nicht, hat sie oben nichts geprueft. */ await gast.seite.evaluate(() => { document.getElementById("saal-satz").textContent += " ZZTESTWORTZZ"; }); const leck = await leckLesen(gast.seite, ["ZZTESTWORTZZ"]); ok(leck.length === 1, "Gegenprobe: ein Wort im Dokument wird gefunden"); /* (f) Die Reihen-Messung muss eine AUFGEDECKTE Reihe auch sichtbar nennen -- sonst waere "der Plan steht nicht da" oben nur die Folge davon, dass sie nie etwas findet. */ await gast.seite.evaluate(() => { document.getElementById("reihe-plan").hidden = false; }); await gast.seite.waitForTimeout(120); const sichtbar = await reihenLesen(gast.seite); ok(sichtbar.plan !== null, "Gegenprobe: eine aufgedeckte Reihe gilt als sichtbar"); await gast.kontext.close(); } /* ===================================================================== SCHLUSS ===================================================================== */ await browser.close(); await new Promise((r) => vorbau.close(r)); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } /* DIE ANZAHL GEHOERT IN DIE BEDINGUNG. Faende dieses Werkzeug keine einzige Messung, waere "0 Fehler" das beste Ergebnis ueberhaupt -- und voellig wertlos. */ console.log(`\n${geprueft} Messungen, ${fehler} Fehler`); if (geprueft < 40) { console.log(`NICHT NACHSEHBAR: nur ${geprueft} Messungen gelaufen, erwartet mindestens 40.`); process.exit(3); } if (fehler) { console.log("NICHT IN ORDNUNG"); process.exit(1); } console.log("ALLES IN ORDNUNG"); process.exit(0);