Files
dogfather-universe/server/mess-quer.mjs
T
DogFatherGit e272dd4673 Am Handy ist die Transportleiste wieder erreichbar
Der offene Punkt von gestern Nacht, jetzt geloest -- und dabei stellte
sich heraus, dass er schlimmer war als gemeldet.

WAS WIRKLICH LOS WAR

Gemeldet war: "Am Handy bleibt bei offener Regie vom Bild nichts."
Gemessen auf 390x844 ergab sich mehr:

    Saal 69..844 (775 px)
    Zeilen  101 + 0 + 511 + 325 = 937
    transport 681..1006  -- 162 Pixel UNTER dem Fensterrand

Der Koerper hat `overflow: hidden`; die Leiste war also nicht
abgeschnitten, sondern weg. Play, "Naechstes" und die Sprungknoepfe:
bei offener Regie nicht erreichbar. Das Bild hatte dabei null Pixel.

DIE RECHNUNG, DIE ES ERKLAERT

    Regieleiste   101  (zwei Zeilen Register)
    Regie-Kopf    207  (fuenf Zeilen zu je rund 44 px -- der
                        Klapp-Knopf stand allein in einer)
    Regie-Inhalt  305
    Transport     325  (Schiene 48 + drei Gruppen untereinander)
                 ----
                  938  von 775.

Video, Regie und Transport passen auf einem Handy nicht nebeneinander.
Das ist keine Frage der Gestaltung, sondern des Platzes.

VIER SCHNITTE UND EINE ENTSCHEIDUNG

  1. Die Tempo-Gruppe klappt hinter ihren eigenen Wert -- dasselbe
     Muster wie "Aa" im Chat, das dort 73 Pixel gespart hat. Der
     Knopf zeigt "1x" oder "1,5x", man muss zum Nachsehen also nicht
     aufklappen. Am Rechner gibt es ihn nicht.
  2. Der Klapp-Knopf rutscht neben die Lampe statt in eine eigene
     Zeile.
  3. Die drei Gruppen der Transportreihe stehen nebeneinander (158
     statt 240 Pixel) -- moeglich, seit die Tempo-Gruppe klappt.
  4. Die Messwerte stehen in einer Zeile, die grossen Knoepfe
     bekommen weniger Polsterung.

Das reichte nicht. Also die Entscheidung: AM HANDY LEGT SICH DIE
REGIE UEBER DAS BILD, statt ihm Platz wegzunehmen -- als Rasterfeld
ueber die Zeilen 2 und 3, unten angedockt, hoechstens 72 Prozent
hoch. Oben bleibt ein Streifen Bild stehen (gemessen 135 px): Wer
mitten in der Sendung etwas einstellt, muss sehen, worueber er redet.

Gemessen jetzt: Saal 775 = Leiste 101 + Bild 481 + Regie 346, und
der Transport steht bei 601..844 -- erreichbar.

DREI VERSUCHE, DIE ES NICHT WURDEN (und warum)

  - `position: absolute` mit gemessener Transporthoehe: lief, lag
    aber 17 Pixel ueber den Registern. Der Bezugsrahmen eines
    absolut gesetzten RASTERFELDES ist sein Rasterbereich, nicht der
    Container -- mit `grid-row: 3` (0 Pixel hoch) wurde aus
    `max-height: 100%` eine Hoehe von einem Pixel.
  - `top` UND `bottom` UND `height: fit-content`: Der Browser
    verwirft dann das `bottom`; die Regie ragte 101 Pixel in die
    Leiste.
  - `grid-row: 2 / 4` mit `align-self: end` statt absolut: ragte 146
    Pixel nach oben und schob die Seite 198 Pixel breiter.

AUF 320 PIXELN BLEIBT ES BEIM ALTEN

Dort passen Register (104), Regie-Kopf (210) und Transportleiste
(158) zusammen nicht in die 499 Pixel Saal -- es fehlten 17. Fuenf
Verteilungen haben nur bestimmt, WER verdeckt wird: erst
"Vorbereiten" und "Beenden" unter der Leiste, dann die Schublade
ueber "Gaeste" und "Bild". Die Schublade greift deshalb erst ab 360
Pixeln; darunter bleibt der Stand von vorher -- ein bekannter Mangel,
aber kein neuer. Der Grund steht im Stilblatt.

NEBENBEI GEFUNDEN

  - `.muenzsatz` brach nicht um und ragte auf 320 px 8 Pixel hinaus
    -- die Tafel rollte dort wieder waagerecht.

WACHEN GESCHAERFT

  - Die Klammerwache von gestern hat heute ihren ersten echten Fund
    gemacht: Beim Herausschneiden einer Regel blieb eine `}` stehen.
    Ohne sie waere das als drei Befunde in `mess-reaktion`
    aufgetaucht, die wie Programmfehler ausgesehen haetten.
  - Der Namensstreit-Waechter meldete `pult (grid vs. flex)` und
    `messwerte__paar (grid vs. flex)` -- beides Fehlalarm: Eine
    Regel in einem Zusammenhang (`.saal .pult`) und eine in einer
    Medienabfrage sind Varianten, kein Streit. Er nimmt beides jetzt
    aus. Dabei fiel auf, dass sein Muster die schliessende Klammer
    verbrauchte, die der naechste Treffer als Anfang braucht -- der
    echte Fall vom 28.09. (`.stufen` zweimal) wurde dadurch gar
    nicht gefunden. Die Gegenprobe deckt jetzt fuenf Faelle ab.
  - `mess-reaktion` mass die HOEHE der Kinozeile und haette 431
    Pixel gemeldet, waehrend das Bild vollstaendig verdeckt war.
    Sie misst jetzt, wieviel oberhalb der Schublade uebrig bleibt.

GEPRUEFT
  pruef-reaktion, -css-klassen, -tippziele, -handy, -struktur,
  -buehne: alle EXIT 0
  mess-reaktion  EXIT 0, kein ACHTUNG, kein offener Punkt
  mess-quer      EXIT 0 -- fuenf Groessen, nichts rollt seitlich
  mess-buehne    EXIT 0
2026-09-29 12:56:03 +02:00

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";
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],
["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);