DER BEFUND
Im naechtlichen Lauf standen dreizehn Pruefungen mit derselben Zeile
rot:
[uncaughtException] Error: spawnSync openssl ENOENT
Kein Programmfehler. Die Pruefungen brauchen einen https-Vorbau (der
Sitzungskeks des Hauses ist `secure` und kommt ueber http nicht an),
und dafuer erzeugen sie ein Wegwerf-Zertifikat mit openssl.
GEMESSEN, NICHT VERMUTET
openssl liegt hier: C:\Program Files\Git\mingw64\bin\openssl.exe
C:\Program Files\Git\usr\bin\openssl.exe
im System-PATH steht: C:\Program Files\Git\cmd (nur git.exe)
im Benutzer-PATH: nichts mit Git
Wer aus Git Bash startet, erbt die mingw-Pfade und merkt nie etwas.
Die Aufgabenplanung startet `node.exe` DIREKT, ohne Shell -- und
bekommt sie nicht. Bei mir gruen, nachts rot, und der Grund steht
nicht im Code.
Das Schlimmste daran ist nicht der Ausfall: Dreizehn dauerhaft rote
Zeilen in einer Notiz, die Filipe morgens liest, gewoehnen einem das
Hinsehen ab -- und decken dabei die echten Befunde zu.
DIE REPARATUR
`server/helfer-openssl.mjs` sucht openssl erst im PATH (ohne `where`
oder `which` -- beides sind selbst Programme und koennen genauso
fehlen), danach an den Orten, an denen es auf einem Windows-Rechner
mit Git wirklich liegt.
Dazu `zertifikatBauen(schluessel, zertifikat, host)`. Die acht
Aufrufformen im Haus waren identisch bis auf die Variablennamen
(schl/zert, schluesselDatei/zertDatei, zKey/zCrt, sk/zt, key/crt) --
eine Stelle traegt alle neunundzwanzig. Zwei Tage zuvor war eine
davon um ein `-addext` aermer als die anderen; gemerkt hat es
niemand, weil der Browser den fehlenden Alternativnamen erst bei
einer Weiterleitung anmahnt. Eine Stelle kann nicht von sich selbst
abweichen.
KEIN EINTRAG IN DEN SYSTEM-PATH. `mingw64\bin` enthaelt rund hundert
Programme mit Unix-Namen (`find`, `sort`, `link`), die gleichnamige
Windows-Befehle verdecken. Das fuer eine Pruefung zu aendern haette
an ganz anderen Stellen Fehler verursacht, die niemand hierher
zurueckverfolgt.
GEGENPROBE IN BEIDE RICHTUNGEN, mit dem PATH des Nachtlaufs:
vorher (direkter Aufruf): ABSTURZ: spawnSync openssl ENOENT
nachher (ueber den Helfer): Zertifikat gebaut, 1236 Bytes
Und eine echte Pruefung unter denselben Bedingungen:
PATH ohne mingw64 -> pruef-chat-neu-stelle
63 Pruefungen, 0 Fehler, 0 Abstuerze
Genau diese Datei stand im Nachtlauf mit ENOENT rot.
EINE WACHE DAGEGEN
pruef-struktur prueft ab jetzt, dass niemand openssl wieder direkt
ruft -- der naechste merkt es dort und nicht erst in einem
Nachtlauf, den niemand liest. Drei Gegenproben.
Beim ersten Anlauf schlug sie auf ihre EIGENEN Probetexte an: Sie
liest alle Serverdateien, und dazu gehoert sie selbst. Die Texte
werden jetzt zusammengesetzt. Derselbe Selbsttreffer ist mir heute
schon zweimal passiert.
DER DRITTE AUSGANG BLEIBT, WO ER HINGEHOERT
`helfer-kachel-echtfarbe.mjs` faengt den Fehler weiterhin ab und
meldet `moeglich: false` mit Grund, statt abzubrechen. Eine
Sicherung abzuschaffen, weil ihr Anlass gerade behoben ist, ist der
Anfang des naechsten stillen Fehlschlags.
NEBENBEI: pruef-community-sicht
Sie war rot mit "ZU VIEL: reaktion.html" -- mein eigener Rueckstand
vom 28.09. Die Reaction steht als Kachel im Community-Bereich; die
Liste war nicht nachgezogen. Jetzt 10 / 0.
Die Liste bleibt bewusst von Hand gepflegt: Sie ist eine ABSICHT,
keine Ableitung. Aus rechte.js gelesen verglichen sich zwei Kopien
derselben Quelle -- immer gruen, nie ein Beweis.
GEPRUEFT
pruef-struktur ALLES IN ORDNUNG (mit der neuen Wache)
pruef-community-sicht 10 / 0
pruef-chat-neu-stelle 63 / 0 (mit dem PATH des Nachtlaufs)
pruef-notizen, -teamlage-karten, -werdegang: EXIT 0, keine Abstuerze
mess-quer EXIT 0
29 Dateien: node --check auf allen, kein direkter Aufruf mehr
583 lines
26 KiB
JavaScript
583 lines
26 KiB
JavaScript
/* =====================================================================
|
|
WAS MUSS MAN SEITLICH SCHIEBEN? (28.09.2026)
|
|
|
|
Die Pruefung nebenan misst, was die Schnittstelle antwortet. Das
|
|
sagt nichts darueber, ob ein Mensch damit einen Abend verbringen
|
|
moechte. Dieses Werkzeug oeffnet die Seite in einem echten Browser,
|
|
schaltet sie durch alle drei Zustaende und legt Bilder ab.
|
|
|
|
Gemessen wird dabei auch, was man sonst uebersieht:
|
|
- laeuft etwas in die Konsole?
|
|
- ragt etwas seitlich hinaus?
|
|
- ist der Saal so hoch wie das Fenster, oder muss man scrollen?
|
|
|
|
NIE DIE ECHTE DATENBANK. Eigener Ordner, eigener Port, eigenes
|
|
Zertifikat -- und am Ende ist alles weg.
|
|
|
|
Aufrufen mit: node server/mess-reaktion.mjs
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { request } from "node:http";
|
|
import { execFileSync } from "node:child_process";
|
|
import { createServer as httpsServer } from "node:https";
|
|
import { zertifikatBauen } from "./helfer-openssl.mjs";
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
const ordner = mkdtempSync(join(tmpdir(), "mess-reakt-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
const PORT = 5491;
|
|
const HTTPS_PORT = PORT + 1;
|
|
process.env.PORT = `${PORT}`;
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
for (const art of ["uncaughtException", "unhandledRejection"]) {
|
|
process.on(art, (f) => {
|
|
console.error(`\nABBRUCH (${art}): ${f?.stack || f}`);
|
|
process.exit(8);
|
|
});
|
|
}
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
await import("./index.js");
|
|
await new Promise((r) => setTimeout(r, 1000));
|
|
|
|
const CREW = "crew.dogfather-universe.com";
|
|
|
|
/* ---------- Leute ------------------------------------------------------- */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const kenn = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", kenn, new Date().toISOString());
|
|
const ALT = new Date(Date.now() - 200 * 86400000).toISOString();
|
|
function anlegen(name, rolle, code) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salt, 64,
|
|
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n,"
|
|
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768,
|
|
createHmac("sha256", kenn).update(code).digest("hex"), ALT);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
const CODES = { admin: "CODE-M00-000001", gast: "CODE-M01-000001", gast2: "CODE-M02-000001" };
|
|
const IDS = {
|
|
admin: anlegen("DogFather", "admin", CODES.admin),
|
|
gast: anlegen("Nachtfalter", "gast", CODES.gast),
|
|
gast2: 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(rolle) {
|
|
const a = await roh("/workspace/api/anmelden", {
|
|
method: "POST",
|
|
rumpf: { rolle: rolle === "gast2" ? "gast" : rolle, code: CODES[rolle], alter_ok: true },
|
|
});
|
|
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
|
|
}
|
|
const k = {
|
|
admin: await anmelden("admin"),
|
|
gast: await anmelden("gast"),
|
|
gast2: await anmelden("gast2"),
|
|
};
|
|
|
|
/* ---------- Die Buehne fuellen ------------------------------------------ */
|
|
await roh("/workspace/api/reaktion", {
|
|
method: "PUT", keks: k.admin,
|
|
rumpf: {
|
|
titel: "Wir schauen den neuen Trailer",
|
|
video: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
beginnt_am: new Date(Date.now() + 7 * 60_000).toISOString(),
|
|
},
|
|
});
|
|
|
|
/* ZWEI STUECKE IN DIE WARTESCHLANGE. Eine leere Liste sieht auf
|
|
einem Bildschirmfoto genauso aus wie eine kaputte -- beides ist
|
|
ein Kasten ohne Zeilen. Zwei echte Eintraege zeigen, ob Titel,
|
|
Vorschaubild und die vier Knoepfe je Zeile wirklich ankommen. */
|
|
for (const adresse of ["https://youtu.be/9bZkp7q19f0",
|
|
"https://www.youtube.com/watch?v=kJQP7kiw5Fk"]) {
|
|
await roh("/workspace/api/reaktion/liste", {
|
|
method: "POST", keks: k.admin, rumpf: { video: adresse },
|
|
});
|
|
}
|
|
|
|
const REDEN = [
|
|
["gast", "Bin schon daaa 🐺"],
|
|
["gast2", "Ich auch! Hab extra Popcorn geholt"],
|
|
["gast", "Wie lange noch?"],
|
|
["admin", "Zwei Minuten, ich richte noch die Kamera"],
|
|
["gast2", "Kein Stress, wir sitzen bequem"],
|
|
["gast", "Letztes Mal war so gut, ich hab geheult"],
|
|
];
|
|
|
|
/* ---------- Der Vorbau -------------------------------------------------- */
|
|
const schluesselDatei = join(ordner, "p.key");
|
|
const zertDatei = join(ordner, "p.crt");
|
|
zertifikatBauen(schluesselDatei, zertDatei, CREW);
|
|
|
|
const vorbau = httpsServer({
|
|
key: readFileSync(schluesselDatei), cert: readFileSync(zertDatei),
|
|
}, (von, nach) => {
|
|
const weiter = request({
|
|
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
|
|
headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` },
|
|
}, (antwort) => {
|
|
nach.writeHead(antwort.statusCode || 502, antwort.headers);
|
|
antwort.pipe(nach);
|
|
});
|
|
weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } });
|
|
von.pipe(weiter);
|
|
});
|
|
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
|
|
const BASIS = `https://${CREW}:${HTTPS_PORT}`;
|
|
|
|
const pw = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch({
|
|
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors",
|
|
/* Kamera und Mikrofon ohne Rueckfrage -- und ein bewegtes
|
|
Testbild statt eines schwarzen Rechtecks. Ohne das sieht jede
|
|
Aufnahme aus, als waere die Kamera kaputt. */
|
|
"--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"],
|
|
});
|
|
|
|
/* JEDE GEOEFFNETE SEITE HAELT ZWEI STEHENDE VERBINDUNGEN (der
|
|
Ereignisstrom der Kopfleiste und der dieser Seite). Ein Browser
|
|
laesst je Adresse nur wenige davon zu -- wer seine Seiten nicht
|
|
schliesst, nimmt den spaeteren den Platz weg.
|
|
|
|
GEMESSEN: Die Seiten aus den ersten beiden Abschnitten blieben
|
|
offen, und danach kam Lennys Strom gar nicht mehr zustande. Die
|
|
Signale gingen ins Leere, und die Kamerafenster blieben schwarz --
|
|
ein Messfehler, der wie ein Programmfehler aussah. */
|
|
async function alsWer(rolle, breite, hoehe) {
|
|
const kontext = await browser.newContext({
|
|
viewport: { width: breite, height: hoehe },
|
|
/* MIT FINGER, WENN ES EIN HANDY IST.
|
|
Ohne `hasTouch` meldet der Browser `pointer: fine` -- und
|
|
damit greift KEINE der Regeln aus `@media (pointer: coarse)`.
|
|
Dort stehen im ganzen Haus die 44-Pixel-Beruehrziele und seit
|
|
dem 28.09. auch das Ausblenden der Tastenkuerzel. Eine
|
|
Handy-Messung mit Mauszeiger misst eine Seite, die auf keinem
|
|
Handy so aussieht -- und meldet Hoehen, die dort andere sind. */
|
|
hasTouch: breite <= 860,
|
|
ignoreHTTPSErrors: true,
|
|
permissions: ["camera", "microphone"],
|
|
});
|
|
await kontext.addCookies((k[rolle] || "").split("; ").filter(Boolean).map((z) => {
|
|
const [n, ...r] = z.split("=");
|
|
return { name: n, value: r.join("="), domain: CREW, path: "/" };
|
|
}));
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
const anfragen = [];
|
|
seite.on("request", (r) => { if (/\/api\//.test(r.url())) anfragen.push(r.url().replace(/^https?:\/\/[^/]+/, "")); });
|
|
seite.on("pageerror", (f) => konsole.push(String(f?.message || f)));
|
|
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
|
seite.on("response", (a) => {
|
|
if (a.status() >= 400) konsole.push(`${a.status()} ${a.url().replace(/^https?:\/\/[^/]+/, "")}`);
|
|
});
|
|
return { kontext, seite, konsole, anfragen };
|
|
}
|
|
|
|
async function bild(seite, name) {
|
|
await seite.screenshot({ path: join(HIER, `mess-reaktion-${name}.png`) });
|
|
console.log(` Bild: server/mess-reaktion-${name}.png`);
|
|
}
|
|
|
|
async function vermessen(seite, wo) {
|
|
const m = await seite.evaluate(() => ({
|
|
ueber: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth),
|
|
hoch: Math.max(0, document.documentElement.scrollHeight - document.documentElement.clientHeight),
|
|
stand: document.getElementById("saal")?.dataset.stand || "?",
|
|
schild: document.getElementById("schild-wort")?.textContent || "",
|
|
}));
|
|
console.log(` ${wo}: Stand „${m.stand}", Schild „${m.schild}", `
|
|
+ `seitlich ${m.ueber} px, senkrecht ${m.hoch} px`);
|
|
return m;
|
|
}
|
|
|
|
|
|
/* =====================================================================
|
|
WAS MUSS MAN SEITLICH SCHIEBEN?
|
|
|
|
Filipe, 28.09.2026: „ich will auch nicht dass man sachen nach links
|
|
und rechts schieben muss. perfektion das untereinander. ich will
|
|
niemals irgendwas nach links oder rechts swippen muessen."
|
|
|
|
NICHT GERATEN, SONDERN AUFGEZAEHLT. Ein `grep` nach `overflow-x`
|
|
findet die absichtlichen Roller. Es findet NICHT die Stelle, an der
|
|
ein Inhalt breiter ist als sein Kasten und der Browser von sich aus
|
|
eine Rollleiste anhaengt -- und genau das ist der Fall auf Filipes
|
|
Bildschirmfoto. Dieses Werkzeug geht jedes Element durch und nennt
|
|
jedes, dessen Inhalt breiter ist als sein Platz.
|
|
===================================================================== */
|
|
|
|
/* AUF SENDUNG. Nur dann steht die Seite vollstaendig da: ohne
|
|
laufende Sendung fehlen Chat, Kamerafenster und der halbe
|
|
Transport -- und dann misst man eine Seite, die es im Betrieb
|
|
nie gibt.
|
|
|
|
EIN EIGENER FEHLER, GEFUNDEN AM BILDSCHIRMFOTO: Die erste Fassung
|
|
schickte `PUT`; die Route nimmt aber `POST`. Der Aufruf lief ins
|
|
Leere, die Messung lief weiter, und das Schild sagte noch immer
|
|
„GESCHLOSSEN". Deshalb steht unten eine Nachfrage: Eine Messung,
|
|
die ihre eigene Voraussetzung nicht nachprueft, misst irgendetwas
|
|
und meldet es als Ergebnis. */
|
|
for (const stufe of ["vorbereitung", "live"]) {
|
|
await roh("/workspace/api/reaktion/stand", {
|
|
method: "POST", keks: k.admin, rumpf: { stand: stufe },
|
|
});
|
|
}
|
|
/* Ein Gast dazu -- sonst fehlt die halbe Gaesteliste, und im Saal
|
|
steht nur ein Kamerafenster statt zweier. */
|
|
await roh("/workspace/api/reaktion/gast", {
|
|
method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast2 },
|
|
});
|
|
/* Und ein paar Worte im Chat -- eine leere Schiene kann nicht
|
|
ueberlaufen, und genau das waere ein gruener Haken ohne Inhalt. */
|
|
for (const [wer, text] of REDEN) {
|
|
await roh("/workspace/api/reaktion/chat", { method: "POST", keks: k[wer], rumpf: { text } });
|
|
}
|
|
/* EIN ZWEITES VIDEO -- SONST FEHLT DER UMSCHALTER.
|
|
Dieses Werkzeug meldete „nichts rollt seitlich", waehrend
|
|
`mess-reaktion` am selben Aufbau 76 Pixel fand. Der Unterschied:
|
|
Dort war ein zweites Video eingerichtet, also stand der
|
|
Umschalter in der Leiste -- mit einem ganzen Videotitel darin,
|
|
und genau der lief hinaus. Hier war er `hidden`.
|
|
Ein Knopf, den die Messung nie sieht, kann nicht ueberlaufen --
|
|
und ihr gruener Haken sagt dann nichts ueber ihn. */
|
|
await roh("/workspace/api/reaktion/zweitvideo", {
|
|
method: "POST", keks: k.admin,
|
|
rumpf: { video: "https://www.youtube.com/watch?v=kJQP7kiw5Fk" },
|
|
});
|
|
|
|
{
|
|
const nach = await roh("/workspace/api/reaktion", { keks: k.admin });
|
|
if (nach.j?.stand !== "live") {
|
|
console.error(`ABBRUCH: die Sendung steht auf „${nach.j?.stand}" statt „live" -- `
|
|
+ "ohne Sendung fehlen Chat und Kamerafenster, und dann misst dieses "
|
|
+ "Werkzeug eine Seite, die es im Betrieb nicht gibt.");
|
|
process.exit(9);
|
|
}
|
|
console.log(`Buehne steht: auf Sendung, ein Gast, Chat gefuellt, `
|
|
+ `zweites Video ${nach.j?.zweit?.video ? "bereit" : "FEHLT"}.`);
|
|
if (!nach.j?.zweit?.video) {
|
|
console.error("ABBRUCH: ohne zweites Video fehlt der Umschalter in der "
|
|
+ "Transportleiste -- und genau der lief am 28.09. seitlich hinaus.");
|
|
process.exit(9);
|
|
}
|
|
}
|
|
|
|
const GERAETE = [
|
|
["Rechner", 1440, 900],
|
|
["Tablet", 900, 1200],
|
|
["Handy", 412, 915],
|
|
["iPhone", 390, 844],
|
|
["kleines Handy", 360, 640],
|
|
["kleinstes Handy", 320, 568],
|
|
];
|
|
|
|
let befunde = 0;
|
|
|
|
for (const [gname, breite, hoehe] of GERAETE) {
|
|
console.log(`\n=== ${gname} (${breite}x${hoehe}) ===`);
|
|
const host = await alsWer("admin", breite, hoehe);
|
|
await host.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "domcontentloaded" });
|
|
await host.seite.waitForSelector("#reiter", { timeout: 15000 });
|
|
|
|
/* ZUERST ZUGEKLAPPT -- DAS IST DER HAEUFIGSTE ZUSTAND.
|
|
Die erste Fassung mass nur mit offener Regie, weil dort die
|
|
gemeldete Stelle lag. `mess-reaktion` fand daraufhin 76 Pixel
|
|
seitlichen Ueberlauf auf einem 390er Handy, die hier nicht
|
|
auftauchten -- schlicht weil dort die Regie ZU war. Wer nur
|
|
den Zustand misst, in dem der Fehler gemeldet wurde, findet
|
|
den zweiten nie. */
|
|
{
|
|
/* AUSDRUECKLICH ZUKLAPPEN. Die Seite merkt sich den Zustand,
|
|
und beim Laden stand sie hier auf „offen" -- die Messung
|
|
haette den zugeklappten Fall also nie gesehen und trotzdem
|
|
„zugeklappt: 0 px" gemeldet. Ein gruener Haken ueber einen
|
|
Zustand, den man gar nicht hergestellt hat. */
|
|
await host.seite.evaluate(() => {
|
|
const p = document.getElementById("pult");
|
|
if (p && p.dataset.auf === "ja") document.getElementById("pult-griff")?.click();
|
|
});
|
|
await host.seite.waitForTimeout(400);
|
|
const zu = await host.seite.evaluate(() => ({
|
|
auf: document.getElementById("pult")?.dataset.auf,
|
|
ueber: Math.max(0, document.documentElement.scrollWidth
|
|
- document.documentElement.clientWidth),
|
|
breitestes: (() => {
|
|
let b = null, max = 0;
|
|
for (const e of document.querySelectorAll("*")) {
|
|
const r = e.getBoundingClientRect();
|
|
if (r.right > max) { max = r.right; b = e; }
|
|
}
|
|
return b ? `${b.tagName.toLowerCase()}${b.id ? "#" + b.id : ""}`
|
|
+ `${typeof b.className === "string" && b.className
|
|
? "." + b.className.trim().split(/\s+/)[0] : ""} bis ${Math.round(max)} px` : "-";
|
|
})(),
|
|
}));
|
|
console.log(` zugeklappt (auf=${zu.auf}): seitlich ${zu.ueber} px`);
|
|
if (zu.ueber > 0) {
|
|
console.log(` ACHTUNG: die Seite rollt bei ZUGEKLAPPTER Regie ${zu.ueber} px seitlich`);
|
|
console.log(` am weitesten rechts: ${zu.breitestes}`);
|
|
befunde++;
|
|
}
|
|
}
|
|
|
|
/* Die Regie aufklappen -- zugeklappt sieht man die Tafeln nicht,
|
|
und genau in ihnen steckt der gemeldete Ueberlauf. */
|
|
const auf = await host.seite.evaluate(() => {
|
|
const p = document.getElementById("pult");
|
|
if (p && p.dataset.auf !== "ja") document.getElementById("pult-griff")?.click();
|
|
return document.getElementById("pult")?.dataset.auf || "?";
|
|
});
|
|
await host.seite.waitForTimeout(400);
|
|
console.log(` Regie: ${auf}`);
|
|
|
|
const register = await host.seite.evaluate(() =>
|
|
[...document.querySelectorAll(".reiter__knopf")].map((b) => b.dataset.tafel));
|
|
console.log(` Register: ${register.join(" · ")}`);
|
|
{
|
|
const z = await host.seite.evaluate(() => {
|
|
const saal = document.getElementById("saal");
|
|
const teile = [...saal.children].filter((e) => e.getBoundingClientRect().height > 0)
|
|
.map((e) => { const q = e.getBoundingClientRect();
|
|
return `${e.id || e.className.split(" ")[0]} ${Math.round(q.top)}..${Math.round(q.bottom)}`; });
|
|
return `Zeilen ${getComputedStyle(saal).gridTemplateRows} | ${teile.join(" / ")}`;
|
|
});
|
|
console.log(` ${z}`);
|
|
}
|
|
|
|
/* Erst die Seite als Ganzes. */
|
|
const seitlich = await host.seite.evaluate(() =>
|
|
Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth));
|
|
if (seitlich > 0) { console.log(` ACHTUNG: die SEITE selbst rollt ${seitlich} px seitlich`); befunde++; }
|
|
|
|
for (const reg of register) {
|
|
await host.seite.evaluate((r) => {
|
|
document.querySelector(`.reiter__knopf[data-tafel="${r}"]`)?.click();
|
|
}, reg);
|
|
await host.seite.waitForTimeout(250);
|
|
|
|
const quer = await host.seite.evaluate(() => {
|
|
const raus = [];
|
|
for (const el of document.querySelectorAll("*")) {
|
|
const w = el.scrollWidth - el.clientWidth;
|
|
if (w <= 1) continue;
|
|
const r = el.getBoundingClientRect();
|
|
if (r.width === 0 || r.height === 0) continue; /* verborgen */
|
|
const st = getComputedStyle(el);
|
|
if (st.display === "none" || st.visibility === "hidden") continue;
|
|
|
|
/* NUR WAS MAN WIRKLICH SCHIEBEN KANN.
|
|
`overflow-x: hidden` ist KEIN Wischen -- dort ist der Text
|
|
abgeschnitten (meist mit „…"), und schieben laesst sich
|
|
nichts. Das ist ein anderes Thema und gehoert nicht in
|
|
diese Messung; wer es mitzaehlt, meldet 78 Stellen, von
|
|
denen 60 gar keine sind, und findet die echten nicht mehr.
|
|
`visible` zaehlt ebenfalls nicht als Stelle -- dort wandert
|
|
der Ueberlauf nach oben zum naechsten Roller, und DER ist
|
|
die Stelle. Ausnahme: Wurzel und Rumpf, denn dort rollt
|
|
das Fenster selbst. */
|
|
const wurzel = el === document.documentElement || el === document.body;
|
|
if (!wurzel && st.overflowX !== "auto" && st.overflowX !== "scroll") continue;
|
|
raus.push({
|
|
wer: el.tagName.toLowerCase()
|
|
+ (el.id ? "#" + el.id : "")
|
|
+ (el.className && typeof el.className === "string"
|
|
? "." + el.className.trim().split(/\s+/).join(".") : ""),
|
|
ueber: w,
|
|
innen: el.clientWidth,
|
|
inhalt: el.scrollWidth,
|
|
ox: st.overflowX,
|
|
/* Was ist das breiteste Kind? Das ist fast immer die
|
|
Ursache -- und ohne diese Angabe sucht man sie von Hand. */
|
|
/* DIE KETTE BIS ZUR URSACHE. Das breiteste Kind zu nennen
|
|
reicht nicht: Es ist meist selbst nur ein Kasten, der
|
|
sich nach SEINEM breitesten Kind richtet. Erst am Ende
|
|
der Kette steht das Ding, das wirklich zu breit ist --
|
|
und nur dort kann man etwas reparieren. */
|
|
kette: (() => {
|
|
const teile = [];
|
|
let jetzt = el;
|
|
for (let tief = 0; tief < 8; tief++) {
|
|
let b = null, max = 0;
|
|
for (const kind of jetzt.children) {
|
|
const kr = kind.getBoundingClientRect();
|
|
if (kr.width > max) { max = kr.width; b = kind; }
|
|
}
|
|
if (!b) break;
|
|
teile.push(`${b.tagName.toLowerCase()}${b.id ? "#" + b.id : ""}`
|
|
+ `${typeof b.className === "string" && b.className
|
|
? "." + b.className.trim().split(/\s+/)[0] : ""}:${Math.round(max)}`);
|
|
jetzt = b;
|
|
}
|
|
return teile.length ? teile.join(" > ") : "-";
|
|
})(),
|
|
});
|
|
}
|
|
return raus;
|
|
});
|
|
|
|
/* EIN BILD JE REGISTER AUF DEM RECHNER.
|
|
Zahlen sagen, dass nichts mehr rollt. Sie sagen nicht, ob es
|
|
danach gut aussieht -- und genau das war Filipes Frage. */
|
|
if (breite === 1440) {
|
|
await host.seite.locator("#pult-innen").screenshot({
|
|
path: join(HIER, `mess-quer-tafel-${reg}.png`) }).catch(() => {});
|
|
/* Die Stufenleiter steht weiter unten als das Fenster reicht --
|
|
sie braucht ein eigenes Bild. Ausgerechnet sie trug bis heute
|
|
den Stil der Chat-Leiste (Namensstreit), also ist sie die
|
|
Stelle, an der man nachsehen muss. */
|
|
if (reg === "gestaltung") {
|
|
await host.seite.locator("#stufen").screenshot({
|
|
path: join(HIER, "mess-quer-stufen.png") }).catch(() => {});
|
|
}
|
|
}
|
|
if (quer.length === 0) { console.log(` ${reg}: nichts rollt seitlich`); continue; }
|
|
console.log(` ${reg}: ${quer.length} Stelle(n) rollen seitlich`);
|
|
for (const q of quer) {
|
|
console.log(` ACHTUNG ${q.wer}`);
|
|
console.log(` Platz ${q.innen} px, Inhalt ${q.inhalt} px `
|
|
+ `(${q.ueber} px zu viel), overflow-x: ${q.ox}`);
|
|
console.log(` Kette: ${q.kette}`);
|
|
/* WER RAGT UEBER DIE RECHTE KANTE?
|
|
Die erste Fassung fragte nach der BREITE eines Kindes --
|
|
und fand nichts, obwohl 211 Pixel ueberliefen. Der Grund:
|
|
Ein Kind kann genau passen und sein ENKEL trotzdem rechts
|
|
hinausragen, weil dazwischen `overflow: visible` steht.
|
|
Gefragt ist also nicht „wer ist zu breit", sondern „wessen
|
|
rechte Kante liegt jenseits der Kante des Rollers" -- und
|
|
davon das oberste Glied, denn alles darunter folgt ihm. */
|
|
const schuld = await host.seite.evaluate((wer) => {
|
|
const nenne = (e) => e.tagName.toLowerCase()
|
|
+ (e.id ? "#" + e.id : "")
|
|
+ (e.className && typeof e.className === "string"
|
|
? "." + e.className.trim().split(/\s+/).join(".") : "");
|
|
const roller = [...document.querySelectorAll("*")].find((e) => nenne(e) === wer);
|
|
if (!roller) return ["(nicht gefunden)"];
|
|
const kante = roller.getBoundingClientRect().left + roller.clientWidth;
|
|
const zeilen = [];
|
|
/* IMMER ABSTEIGEN, AUCH DURCH UNAUFFAELLIGES.
|
|
Die erste Fassung stieg nur in Kinder hinab, die selbst
|
|
schon hinausragten -- und fand deshalb gar nichts: Die
|
|
sichtbare Tafel passte (327 px in 359), und erst IHR Kind
|
|
ragte 572 px weit hinaus. Eine Suche, die nur dort
|
|
weitersucht, wo sie schon fuendig ist, findet nie etwas,
|
|
das hinter etwas Unauffaelligem steckt. */
|
|
const gehe = (el, tief) => {
|
|
if (tief > 7) return;
|
|
for (const kind of el.children) {
|
|
const kr = kind.getBoundingClientRect();
|
|
if (kr.width === 0) continue;
|
|
if (kr.right <= kante + 1) { gehe(kind, tief); continue; }
|
|
const st = getComputedStyle(kind);
|
|
zeilen.push(`${" ".repeat(tief)}${nenne(kind).split(".").slice(0, 2).join(".")}`
|
|
+ ` ragt ${Math.round(kr.right - kante)} px hinaus`
|
|
+ ` (${Math.round(kr.width)} px breit)`
|
|
+ ` · ${st.display}`
|
|
+ (st.gridTemplateColumns !== "none" ? ` · spalten ${st.gridTemplateColumns}` : "")
|
|
+ (st.display.includes("flex") ? ` · wrap ${st.flexWrap} · basis ${st.flexBasis}` : "")
|
|
+ ` · min-width ${st.minWidth}`
|
|
+ (st.whiteSpace !== "normal" ? ` · ${st.whiteSpace}` : ""));
|
|
gehe(kind, tief + 1);
|
|
}
|
|
};
|
|
gehe(roller, 0);
|
|
return zeilen.length ? zeilen : ["(nichts ragt hinaus -- Rand oder Schatten?)"];
|
|
}, q.wer);
|
|
for (const z of schuld) console.log(` ${z}`);
|
|
befunde++;
|
|
}
|
|
}
|
|
|
|
/* ==== UND IN JEDER ANORDNUNG ====================================
|
|
`mess-reaktion` meldete auf einem 390er Handy 76 Pixel
|
|
seitlichen Ueberlauf, die hier nicht auftauchten. Der
|
|
Unterschied war die ANORDNUNG: Dort war vorher „nur Kamera"
|
|
eingestellt, hier stand die Vorgabe. Die Anordnung gehoert der
|
|
Sendung und gilt fuer alle -- wer sie nicht durchprobiert,
|
|
misst nur einen von fuenf Faellen. */
|
|
for (const anordnung of ["kino", "gleich", "kameras", "nur_video", "nur_kamera"]) {
|
|
await host.seite.evaluate((a) => {
|
|
document.querySelector(`#layout-reihe .layout-knopf[data-layout="${a}"]`)?.click();
|
|
}, anordnung);
|
|
await host.seite.waitForTimeout(500);
|
|
const m = await host.seite.evaluate(() => {
|
|
const ueber = Math.max(0, document.documentElement.scrollWidth
|
|
- document.documentElement.clientWidth);
|
|
if (ueber === 0) return { ueber };
|
|
let b = null, max = 0;
|
|
for (const e of document.querySelectorAll("*")) {
|
|
const r = e.getBoundingClientRect();
|
|
if (r.width === 0) continue;
|
|
if (r.right > max) { max = r.right; b = e; }
|
|
}
|
|
return {
|
|
ueber,
|
|
wer: b ? `${b.tagName.toLowerCase()}${b.id ? "#" + b.id : ""}`
|
|
+ `${typeof b.className === "string" && b.className
|
|
? "." + b.className.trim().split(/\s+/)[0] : ""}` : "-",
|
|
bis: Math.round(max),
|
|
};
|
|
});
|
|
if (m.ueber > 0) {
|
|
console.log(` ACHTUNG Anordnung „${anordnung}": ${m.ueber} px seitlich `
|
|
+ `-- am weitesten rechts ${m.wer} (bis ${m.bis} px)`);
|
|
befunde++;
|
|
} else {
|
|
console.log(` Anordnung „${anordnung}": seitlich 0 px`);
|
|
}
|
|
}
|
|
await host.seite.evaluate(() => {
|
|
document.querySelector('#layout-reihe .layout-knopf[data-layout="kino"]')?.click();
|
|
});
|
|
|
|
await host.seite.screenshot({ path: join(HIER, `mess-quer-${breite}.png`) });
|
|
await host.kontext.close();
|
|
}
|
|
|
|
console.log(`\n=== ${befunde === 0 ? "NICHTS rollt seitlich." : befunde + " Stelle(n) rollen seitlich."} ===`);
|
|
|
|
await browser.close();
|
|
await new Promise((r) => vorbau.close(r));
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Windows haelt die Datei noch */ }
|
|
process.exit(befunde === 0 ? 0 : 1);
|