GEMESSEN, BEVOR ICH ETWAS ANGEFASST HABE:
204 pruef-Dateien, abgeleitete Ports 5000-5409 (dicht belegt)
24 mess-Dateien, Nummern von Hand: 4471 bis 5493
davon IM Pruefbereich: 5387, 5397, 5397, 5399, 5399, 5401, 5403, 5405
untereinander doppelt: 5397, 5399, 5461, 5483, 5491
Aufgefallen ist es, weil mess-buehne und mess-reaktion beide auf 5483
lagen und ein haengengebliebener Lauf gestern einen ganzen Messlauf
gekostet hat.
Beim Umbau kam das Groessere heraus: EINUNDZWANZIG der 24 Messdateien
hatten nicht nur eine Nummer von Hand, sondern ueberhaupt keinen
Waechter -- schlicht `const PORT = 5397;`. Liegt dort schon ein
Server, startet der eigene still nicht, und gemessen wird ab da ein
fremder Stand. Genau der Fehler, gegen den helfer-port.mjs gebaut
wurde; die Messdateien standen die ganze Zeit ausserhalb.
WAS JETZT GILT
Zwei Sorten, zwei Bereiche, beide abgeleitet aus der Stelle im
Alphabet -- jede Sorte unter ihresgleichen, sonst verschoebe eine
neue Pruefung die Nummern aller Messungen. Pruefungen ab 5000,
Messungen ab MESS_BASIS = 5900.
5900 und nicht 5500: dazwischen bleibt Platz fuer 245 weitere
Pruefdateien (bei 5500 waeren es 45). Nach oben 5948 + AUSWEICHEN
4000 = 9948, also unter 10080, der naechsten gesperrten Nummer.
Nachgerechnet, nicht geschaetzt -- eine geschaetzte 4500 hatte bei
BASIS schon einmal danebengelegen.
Eine Wache dazu: Waechst der Pruefbereich bis an MESS_BASIS heran,
bricht die Ableitung ab und sagt, was zu tun ist. Eine stille
Ueberschneidung waere genau der Fehler, den das hier beseitigt.
Die zweite Nummer kommt ueber nr=1, nie ueber `PORT + 1`:
portNummer ueberspringt gesperrte Nummern, deshalb kann die naechste
Zahl die Nummer der naechsten DATEI sein, sobald einmal eine Sperre
dazwischenliegt. Heute liegt dort keine -- das ist Glueck, kein
Entwurf.
NEBENBEI GEFUNDEN UND MIT REPARIERT
Sechs bild-*.mjs riefen den Waechter und warfen seine Antwort weg:
await portMussFreiSein(4315, "das Bildwerkzeug");
process.env.PORT = "4315";
const BASIS = "http://127.0.0.1:4315";
Er lief, meldete nichts und wirkte nicht. Gibt das System den Port
dauerhaft nicht her, weicht er auf Port + 4000 aus und GIBT DIE NEUE
NUMMER ZURUECK -- diese Werkzeuge hoerten danach trotzdem auf der
alten und stuerzten mit `listen EACCES` ab, also mit genau dem
Fehler, gegen den er gebaut wurde. Dazu stand die Zahl dreimal je
Datei. Jetzt einmal, und die Antwort wird benutzt.
tiktok-videos.mjs hatte den Waechter ABGESCHRIEBEN -- eine kurze
eigene Fassung ohne den dritten Ausgang: Bei EACCES meldete sie
"belegt" und brach ab, statt auszuweichen. Auf diesem Rechner ist
genau das am 23.09. eingetreten (Port 5040, Windows-Dienst).
mess-fokus und mess-notizblock hatten dieselbe Abschrift. Eine
abgeschriebene Sicherung ist dieselbe Falle wie eine abgeschriebene
Liste.
GEPRUEFT
pruef-portnummern 15 -> 41 Pruefungen, 0 Fehler
pruef-ports 8 -> 10 Pruefungen, 456 statt 408 Ports geprobt
node --check auf allen 33 geaenderten Dateien
Gegenproben, die wirklich rot werden:
- eine Messdatei auf eine feste Nummer zurueckgesetzt -> 2 FEHL,
danach wieder 41/0
- die Wache: in einem Wegwerf-Ordner mit 452 pruef-Dateien bricht
eigenerPort ab statt still zu ueberlappen; eine Datei knapp
darunter bekommt weiter ihre Nummer (5846)
- die Erkennungen fuer feste Nummern, PORT + 1 und weggeworfene
Waechterantworten je gegen einen gebauten Rueckschritt
Am echten Verhalten gemessen:
- mess-chat-liste und mess-alle-einzelsicht (beide vorher 5397)
GLEICHZEITIG gestartet: 5906 und 5900, beide exit=0. Vorher war
das unmoeglich.
- die 48 neuen Nummern 5900-5947 auf diesem Rechner durchprobiert:
keine belegt, keine vom System gesperrt
- bild-chat.mjs durchgelaufen, drei Bilder, exit=0
Kein Eingriff am laufenden Dienst: helfer-port.mjs wird von index.js
und workspace.js nicht geladen (nachgesehen), nur von Pruef- und
Messwerkzeugen. Beide Haeuser unberuehrt -- es wird keine Zeile
angefasst, die eine Seite ausliefert.
Co-Authored-By: Claude Opus 5 <[email protected]>
584 lines
26 KiB
JavaScript
584 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");
|
|
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
const PORT = await eigenerPort(import.meta);
|
|
const HTTPS_PORT = await eigenerPort(import.meta, "mess-quer (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";
|
|
|
|
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);
|