/* ===================================================================== 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" }); /* AUF DIE LEISTE WARTEN, NICHT AUF DIE REGISTER (01.10.2026). Seit die Register am Fingergeraet hinter einem Knopf liegen, ist `#reiter` dort unsichtbar -- `waitForSelector` wartet aber auf SICHTBARKEIT und lief in die Zeitsperre ("33 x locator resolved to hidden"). Gewartet wird jetzt auf die Leiste selbst; die steht in jedem Fall. */ await host.seite.waitForSelector("#regieleiste", { timeout: 15000 }); /* UND DANN AUFKLAPPEN WIE EIN MENSCH. Diese Messung sucht seitlichen Ueberlauf IN den Registern -- sie muss sie also sehen. Der Knopf gibt es nur am Fingergeraet; am Rechner steht die Leiste ohnehin offen, und `?.` faengt das ab. */ await host.seite.evaluate(() => { const l = document.getElementById("regieleiste"); if (l && l.dataset.auf !== "auf") document.getElementById("reiter-auf")?.click(); }); 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) => { /* ERST AUFKLAPPEN, DANN WAEHLEN. Ein Klick auf ein Register klappt die Leiste am Handy wieder zu -- das naechste waere sonst nicht anklickbar. */ const leiste = document.getElementById("regieleiste"); if (leiste && leiste.dataset.auf !== "auf") { document.getElementById("reiter-auf")?.click(); } 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);