Files
dogfather-universe/server/mess-quer.mjs
T
DogFatherGit a9e0834cfb Nichts wird mehr nach links oder rechts geschoben
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." Dazu ein
Bildschirmfoto der Tafel "Gestaltung" mit waagerechter Rollleiste.

GEMESSEN, NICHT GERATEN

Ein grep nach `overflow-x` findet nur die absichtlichen Roller. Er
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 war auf dem Bild zu sehen. `server/mess-quer.mjs` geht
deshalb im echten Browser jedes Element durch, auf fuenf
Bildschirmgroessen, in allen sieben Registern, bei offener und
zugeklappter Regie und in allen fuenf Anordnungen.

Erster Lauf: 78 Stellen. Davon waren 54 KEINE -- `overflow-x: hidden`
ist abgeschnittener Text, dort laesst sich nichts schieben. Die
Messung trennt das jetzt; wer es mitzaehlt, findet die echten nicht
mehr. Uebrig blieben vier Ursachen:

  1. DIE REGISTER rollten absichtlich waagerecht. Auf 412 px standen
     von 605 px Registern 193 rechts ausserhalb -- dass es
     "Gestaltung" und "Spenden" gibt, erfuhr man nur beim Wischen auf
     Verdacht. Sie brechen jetzt um. Das kostet oben rund 45 Pixel
     und bringt Gewissheit dafuer.

  2. `.stufen` STAND ZWEIMAL IN reaktion.css -- einmal fuer die
     Stufenleiter der Spenden (`display: grid`), 600 Zeilen spaeter
     fuer die Chat-Leiste Offen/Team/Zu (`display: flex`). Die
     spaetere gewinnt: Die Stufenleiter stellte ihre drei Stufen
     NEBENEINANDER, 572 Pixel in einer 327 Pixel schmalen Spalte.
     Das ist die Rollleiste auf Filipes Bild. Die Chat-Leiste heisst
     jetzt `.chatstufen` / `.chatstufe`.
     Dritter Fall dieser Art nach `.tafel` und `.stufe-knopf`.

  3. DIE TAFELN KONNTEN NICHT SCHRUMPFEN. Ein Gitterfeld hat von
     sich aus `min-width: auto` und besteht auf seinem unteilbarsten
     Inhalt. Mit `minmax(0, 1fr)` und `min-width: 0` bricht jetzt
     alles um, statt hinauszulaufen.

  4. DIE TRANSPORTLEISTE ragte am Handy 76 Pixel ueber beide Kanten
     ("Naechstes" war nur halb zu sehen). Zwei Gruende: `.transport__teil`
     hatte kein `flex-wrap` (die mittlere Gruppe schon -- zwei Regeln
     fuer dieselbe Sache, eine vergessen), und im Umschalter steht
     ein ganzer Videotitel, der als Flex-Kind auf seiner vollen
     Breite bestand.

NEBENBEFUND: EINE FESTE ZAHL VON GESTERN

Der Saal stand auf `height: calc(100dvh - 69px)` -- 69 war einmal
die gemessene Hoehe der Kopfleiste. Sie ist 73 geworden, und der
Saal endete damit 4 Pixel unter dem Fensterrand: Der Play-Knopf war
nur mit Scrollen zu erreichen. Die Antwort ist keine neue Zahl,
sondern eine Regel, die misst -- der Koerper ist jetzt eine Spalte,
die Kopfleiste nimmt, was sie braucht, der Saal bekommt den Rest.
(Dabei noch ein Spezifitaetsunfall: `.reaktion-seite` (0,1,0)
verliert gegen `body.start` (0,1,1) aus start.css.)

NEUE WACHEN

  - `mess-quer.mjs` mit Gegenprobe (ohne die Reparaturen findet sie
    47 px, mit ihnen nichts).
  - pruef-reaktion: kein absichtliches `overflow-x: auto` mehr; und
    zwei Grundregeln derselben Klasse mit verschiedenem `display`
    sind ein Namensstreit. Der bisherige Waechter verglich nur
    ZWISCHEN Stilvorlagen -- `.stufen` stand zweimal in DERSELBEN.
  - pruef-css-klassen: jede Klammer hat ihr Gegenstueck. Beim
    Umschreiben blieb heute das Ende einer Regel ohne ihren Anfang
    stehen; der Browser wirft die Zeile weg und schliesst dafuer den
    naechsten @media-Block zu frueh. Drei Befunde sahen daraufhin wie
    Programmfehler aus.

MESSUNG NACHGEZOGEN

`mess-reaktion` stammte noch aus der Zeit vor "Regie links" und
"drei Kacheln" und meldete drei Dinge, die keine Fehler waren --
gemessen gegen den Stand von HEAD: identisch rot. Sie misst ausserdem
seit heute mit FINGER statt Mauszeiger; ohne `hasTouch` greift keine
einzige Regel aus `@media (pointer: coarse)`, und dort stehen alle
44-Pixel-Beruehrziele des Hauses.

OFFEN UND AUFGESCHRIEBEN

Am Handy bleibt bei offener Regie vom Bild nichts (0 px). Der Mangel
besteht seit dem Umbau "Regie links"; drei Versuche, ihn heute zu
loesen, haben Knoepfe verdeckt -- und ein verdeckter Knopf ist
schlimmer als ein kleines Bild. Die Versuche und der richtige Weg
(Tempo-Gruppe hinter einen Knopf, wie "Aa" im Chat) stehen in
reaktion.css. `mess-reaktion` meldet ihn als dritten Ausgang: nicht
als Fehler und nicht als bestanden.

GEPRUEFT
  pruef-reaktion     384 / 0   (vorher 379)
  pruef-spenden      172 / 0
  pruef-buehne        36 / 0
  pruef-css-klassen, -tippziele, -struktur, -handy: ohne Befund
  mess-quer          EXIT 0 -- 28 Stellen, fuenf Groessen, nichts rollt
  mess-reaktion      EXIT 0, 1 benannter offener Punkt
  mess-buehne        EXIT 0
2026-09-29 02:34:09 +02:00

573 lines
25 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";
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");
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
"-keyout", schluesselDatei, "-out", zertDatei, "-days", "2",
"-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`], { stdio: "ignore" });
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],
];
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(" · ")}`);
/* 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);