Nach Filipes sieben Bildschirmfotos vom 07.10.2026. Sieben Wuensche,
und einer davon ("alles viel hochwertiger") betrifft alle anderen --
deshalb steht das Fundament zuerst und nicht am Ende.
DAS AUSSEHEN HAT EINEN WERKSTOFF. Jede Flaeche sagt jetzt, ob sie oben
oder unten liegt: Was obenauf liegt, bekommt oben eine helle Kante und
darunter einen Schatten; was vertieft ist, genau umgekehrt. Dazwischen
gibt es nichts mehr. Vorher stand im ganzen Pult zweimal eine
Lichtkante und sonst nirgends -- genau deshalb sahen diese zwei Stellen
gut aus und alles daneben danebengestellt. Dazu: jeder Knopf senkt sich
beim Druecken, jedes Feld ist ein Loch und kein Kasten, und wer mit der
Tastatur bedient, wird zum ersten Mal ueberhaupt gesehen.
UND EIN FUND DABEI: Die drei Kacheln des Regieplatzes hiessen in der
HTML anders als im Stilblatt (kachel--laeuft gegen regiekachel--laeuft,
regieregiekachel__kopf gegen regiekachel__kopf). Die Aufteilung "links
was laeuft, rechts was man eintraegt" hat damit NIE gegriffen -- und
mess-reaktion hat den falschen Zustand gemessen und gruen gemeldet,
weil sie gegen die HTML geschrieben war.
DIE SPENDENKARTE STEHT IN DER MITTE und kommt aus der Tiefe nach vorn.
Das dreht eine Entscheidung vom 28.09. um ("sie deckt nie die Mitte
zu"); die Begruendung war richtig und ist es noch -- sie schuetzt das
Video, und Filipe sieht die Spende als das, was in dem Augenblick
zaehlt. Der alte Platz bleibt einen Knopf weit weg und gilt fuer Saal
UND OBS-Tafel. Die Karte besteht dafuer aus zwei Teilen: `opacity`
unter 1 zwingt ein Element auf `transform-style: flat`, eine Karte mit
Deckkraft-Uebergang waere also genau waehrend des Hereinkommens flach
und spraenge am Ende in einem Bild in die Tiefe.
FERTIGE SAETZE GEHEN SOFORT INS BILD. Der Browser schickt nur die
Kennung, den Text holt der Server -- deshalb darf hier die Bestaetigung
entfallen und beim freien Text nicht. Zwei Schranken bleiben: sofort
nur mit einem der vier Knopfbetraege (Filipes Ansage galt dem SATZ,
nicht der Summe), und GEZEIGT IST NICHT GEBUCHT -- die Dogen gibt es
erst nach dem Tipp der Leitung, sonst stellt sich jeder seine eigene
Waehrung aus.
WER GESPENDET HAT, GLAENZT IM CHAT -- je nach Hoehe. Die Dauer haengt
an der Stufe und nicht an einer zweiten Leiter; 0 heisst "gar nicht".
GAESTE WERDEN GEFRAGT, NICHT GEHOLT. Vorher sprang die Kamera eines
Zuschauers an, sobald jemand "Dazuholen" drueckte -- er hat es daran
gemerkt, dass er sich selbst sah. Jetzt gibt es einen Vorraum: eigenes
Bild, Pegel, Kamera und Mikro vorwaehlen, Geraete waehlen, dann "Dabei
sein" oder "Lieber nicht". Zwei Minuten Frist, und sie verfaellt
SICHTBAR -- eine verschwundene Zeile sieht fuer den Host aus wie ein
Verklicker.
DAS MISCHPULT. Hier stand fest `echoCancellation: true,
noiseSuppression: true`. Fuer ein Laptopmikrofon richtig, ueber einem
RODECaster falsch: Das Pult hat Gate, Kompressor und EQ schon gemacht,
und die Echo-Unterdrueckung legt sich danach als zweite Automatik
darueber und duckt, sobald von der Gegenseite Ton kommt -- genau das
Pumpen vom 14.09. Jetzt: Geraetewahl fuer Mikro, Kamera und Ausgabe
(auch fuer Gaeste), ein Pultmodus, und darunter GEMESSEN, was wirklich
ankommt. Mit und ohne Pult.
DAS ZWEITE VIDEO war nie gesperrt -- es war nur unsichtbar. Jetzt
stehen Vorschaubild, Titel und die gemerkte Stelle da, "Jetzt wechseln"
auch von hier aus, und aus der Warteschlange legt ein Knopf eines
bereit.
UND EIN FUND AUF DEM BILDSCHIRMFOTO: Das Band "Ton einschalten" lag auf
derselben Hoehe wie die Senderleiste. Bei vier Knoepfen reichte die
Leiste bis zur Mitte -- vom Mikro-Knopf war nur noch ein "t" zu sehen.
Das war schon vorher so; der fuenfte Knopf hat es sichtbar gemacht.
Dieselbe Lehre wie am 06.09.: keine neue Zahl suchen, sondern die
Begegnung unmoeglich machen. mess-reaktion misst das jetzt als
UEBERLAPPUNG.
GEPRUEFT: pruef-reaktion 423 -> 478 (0 Fehler), pruef-spenden 172 -> 215
(0), pruef-reaktion-schmal 44 (0), pruef-tippziele 13 (0), dazu gruen:
pruef-css-klassen, pruef-alle-wege, pruef-haus-trennung, pruef-agentur,
pruef-deutsche-texte. Keine Zahl ist gefallen.
Drei Doppelungen hat erst das Pruefen gefunden: `satz_unbekannt` gab es
zweimal (Muenzsatz), `saetze` im Spendenstand war belegt (ebenfalls
Muenzsaetze), und `glanz_minuten` waere in einem frischen Haus immer 0
geblieben -- die Spalte kam per ALTER TABLE, und das laeuft VOR dem
Anlegen der Werksstufen.
Co-Authored-By: Claude Opus 5 <[email protected]>
430 lines
20 KiB
JavaScript
430 lines
20 KiB
JavaScript
/* DIE REACTION AUF EINEM KLEINEN HANDY (02.10.2026)
|
|
=====================================================================
|
|
|
|
WARUM ES DIESE DATEI GIBT
|
|
|
|
Auf meiner eigenen Merkliste stand seit dem 01.10.: „320x568 zeigt
|
|
noch ein 320x75 grosses Videofeld und eine 1 px schmale
|
|
Chatleiste." Nachgemessen stimmte davon keine einzige Zahl:
|
|
|
|
Regie ZU bei 320x568: Video 127 px, Transport-Unterkante 568
|
|
bei Fensterhoehe 568 -- alles erreichbar
|
|
Regie AUF bei 320x568: Video 0 px, Transport-Unterkante 1585
|
|
Regie AUF bei 390/430: Video unveraendert, Transport am Rand
|
|
|
|
Und die Chatleiste ist auf JEDER Handybreite `display: none` --
|
|
sie liegt dort im Registerstreifen, das ist Absicht und keine
|
|
1-px-Leiste.
|
|
|
|
Eine Bestandsliste ist ein Wegweiser, keine Wahrheit. Auch meine
|
|
eigene.
|
|
|
|
UND WAS SEITDEM PASSIERT IST
|
|
|
|
Filipe: „auf so apps wie youtube und so geht es doch auch auf dem
|
|
handy also perfektionier es endlich."
|
|
|
|
Er hatte recht. Bei offener Regie stand auf 320 px das Pult IM
|
|
RASTER statt darueber -- Bild 0 px, Transportleiste 1017 Pixel
|
|
unter dem Fensterrand. Ab 360 px war genau das laengst geloest;
|
|
es war dieselbe Seite, nur schmaler. Die 360-Pixel-Sperre ist am
|
|
02.10.2026 gefallen (Begruendung und Messreihe in reaktion.css).
|
|
|
|
Gemessen danach, Regie offen:
|
|
|
|
320x568 Bild 180 px · Kopf 147 · Schublade 280 · Inhalt 132
|
|
360x640 Bild 203 px (unveraendert)
|
|
390x844 Bild 219 px (unveraendert)
|
|
|
|
WAS HIER BEWACHT WIRD -- auf JEDER Breite, ohne Ausnahme:
|
|
|
|
1. Das Bild behaelt seine Hoehe, zu UND auf.
|
|
2. Die Transportleiste bleibt im Fenster.
|
|
3. Der Inhalt der Schublade ist erreichbar -- er passt hinein
|
|
oder er rollt. Eine Schublade, deren Inhalt man nicht
|
|
erreicht, verdeckt nur eleganter als vorher.
|
|
4. Die drei grossen Knoepfe behalten ihre 44 Pixel, keiner ist
|
|
abgeschnitten, alle liegen obenauf. Schmaler ja, beschnitten
|
|
nein.
|
|
5. In JEDEM Zustand steht der Knopf, der die Regie wieder
|
|
zumacht, im Fenster UND obenauf.
|
|
|
|
Punkt 2 ist die eigentliche Lehre aus dem Tag: Die alte Fassung
|
|
dieser Datei hat den Mangel unter 360 px nur PROTOKOLLIERT statt
|
|
behauptet, damit eine spaetere Reparatur hier nicht rot wird. Das
|
|
war richtig, solange es keine Reparatur gab -- und ist falsch,
|
|
sobald es eine gibt. Eine Pruefung, die eine Reparatur nicht
|
|
bemerkt, hat sie nie abgesichert.
|
|
|
|
DREI AUSGAENGE: 0 in Ordnung · 1 Befunde · 2 konnte nicht arbeiten.
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-rschmal-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die schmale Reaction");
|
|
process.env.PORT = `${PORT}`;
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
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");
|
|
notbremse(300_000, "pruef-reaktion-schmal");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: "admin", code: "x" }),
|
|
}).catch(() => {});
|
|
for (let i = 0; i < 60; i++) {
|
|
try {
|
|
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
|
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
|
}
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync("CODE-DOGI-0001", 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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
|
.run("Filipe", "admin", hash, salt, 32768, new Date().toISOString());
|
|
d.close();
|
|
|
|
const { chromium } = await import("playwright");
|
|
const browser = await chromium.launch();
|
|
|
|
/* So hoch muss ein Bild mindestens sein, um ein Bild zu sein.
|
|
Gemessen am 02.10.2026 mit offener Regie: 180 px auf 320,
|
|
203 auf 360, 219 auf 390. 100 laesst Luft nach unten, ohne 0 zu
|
|
decken.
|
|
|
|
(Hier stand bis heute `AUS_DEM_FLUSS_AB = 360` -- die Breite, ab
|
|
der die Regie aus dem Fluss genommen wurde. Die Grenze gibt es
|
|
nicht mehr, also auch die Zahl nicht. Eine Konstante, die nichts
|
|
mehr trennt, ist der Anfang einer Erklaerung, die nicht stimmt.) */
|
|
const BILD_MIN = 100;
|
|
|
|
const GERAETE = [[320, 568, "Handy klein"], [390, 844, "Handy"], [430, 932, "Handy gross"]];
|
|
|
|
/** Was am Bildschirm wirklich steht -- in einem Zustand der Regie. */
|
|
const messen = (seite) => seite.evaluate(() => {
|
|
const bild = document.getElementById("leinwand").getBoundingClientRect();
|
|
const transport = document.querySelector(".transport")?.getBoundingClientRect();
|
|
const zu = document.getElementById("reiter-auf");
|
|
const zr = zu?.getBoundingClientRect();
|
|
/* IM FENSTER heisst nicht ANKLICKBAR. `elementFromPoint` fragt, was
|
|
an dieser Stelle wirklich obenauf liegt -- ein Knopf unter einer
|
|
Schublade ist sichtbar und trotzdem nicht zu treffen. */
|
|
const obenauf = zr
|
|
? document.elementFromPoint(zr.left + zr.width / 2, zr.top + zr.height / 2)
|
|
: null;
|
|
return {
|
|
bild: Math.round(bild.height),
|
|
transportUnten: transport ? Math.round(transport.bottom) : null,
|
|
fenster: window.innerHeight,
|
|
zuImFenster: !!zr && zr.height > 0 && zr.top >= 0 && zr.bottom <= window.innerHeight
|
|
&& zr.left >= 0 && zr.right <= window.innerWidth,
|
|
zuObenauf: !!obenauf && (obenauf.id === "reiter-auf" || !!obenauf.closest("#reiter-auf")),
|
|
};
|
|
});
|
|
|
|
for (const [breite, hoehe, wie] of GERAETE) {
|
|
console.log(`\n=== ${wie} (${breite}x${hoehe}) ===`);
|
|
const kontext = await browser.newContext({
|
|
viewport: { width: breite, height: hoehe },
|
|
hasTouch: true, isMobile: true });
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message.slice(0, 90)));
|
|
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click('.rolle[data-rolle="admin"]');
|
|
await seite.fill("#code", "CODE-DOGI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 20000 });
|
|
|
|
/* DIE SENDUNG MUSS LAUFEN. Im Zustand „zu" sind Kino und Chat
|
|
`display: none` -- wer das misst, misst den geschlossenen Saal
|
|
und haelt das Video fuer kaputt. Genau dieser Fehler ist mir
|
|
heute beim ersten Anlauf passiert.
|
|
|
|
Gestellt wird der Zustand ueber die SCHNITTSTELLE, nicht ueber
|
|
`dataset.stand` im Browser: Sonst pruefte die Messung einen
|
|
Zustand, den das Haus so nie herstellt. */
|
|
await seite.evaluate(async () => {
|
|
await fetch("/workspace/api/reaktion", {
|
|
method: "PUT", credentials: "same-origin",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ titel: "Schmale Probe", video: "dQw4w9WgXcQ" }),
|
|
});
|
|
await fetch("/workspace/api/reaktion/stand", {
|
|
method: "POST", credentials: "same-origin",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ stand: "live" }),
|
|
});
|
|
});
|
|
await seite.goto(BASIS + "/workspace/reaktion.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1200);
|
|
|
|
const stand = await seite.evaluate(() => document.getElementById("saal")?.dataset.stand);
|
|
ok(stand === "live", `die Sendung laeuft wirklich (data-stand="${stand}") — sonst misst alles darunter nichts`);
|
|
|
|
/* ==== DIE KAMERAMELDUNG WIRD WEGGERAEUMT, UND ZWAR MIT GRUND ====
|
|
|
|
Ein kopfloser Browser hat keine Kamera. `getUserMedia`
|
|
scheitert, und die Seite schreibt zu Recht „Kamera oder Mikrofon
|
|
sind nicht freigegeben…" in `#fehler`. Gemessen werden soll hier
|
|
aber die Aufteilung einer LAUFENDEN SENDUNG, nicht die Warnung
|
|
eines Rechners ohne Kamera -- sonst misst diese Pruefung eine
|
|
Eigenschaft der Messumgebung und nennt sie einen Befund.
|
|
|
|
WAS DIE MELDUNG KOSTET, IST TROTZDEM GEMESSEN und steht unten im
|
|
Protokoll. Am 02.10.2026:
|
|
|
|
320x568 Bild mit Meldung 56 px · ohne Meldung 127 px
|
|
390x844 Bild 601 px -- die Meldung ist dort 0 px HOCH
|
|
|
|
Beides ist auffaellig und beides gehoert Filipe vorgelegt, nicht
|
|
hier stillschweigend repariert: Auf dem kleinen Handy frisst die
|
|
Meldung 71 Pixel aus einer impliziten FUENFTEN Rasterzeile (der
|
|
Saal deklariert vier), auf dem normalen ist sie ueberhaupt nicht
|
|
zu lesen. Eine Meldung, die erklaert, wie man die Kamera
|
|
freigibt, und die man dabei nicht sehen kann, ist keine. */
|
|
/* GEMESSEN WIRD, WAS SIE KOSTET -- nicht, wie hoch sie gerade ist.
|
|
|
|
Erster Anlauf las die Hoehe des Feldes selbst. Die hing davon ab,
|
|
WANN gelesen wurde: in einem Lauf 62 px, im naechsten 0 px, weil
|
|
die Meldung erst eintrifft, wenn `getUserMedia` aufgegeben hat.
|
|
Eine Protokollzeile, die mal so und mal so ausfaellt, ist
|
|
wertlos.
|
|
|
|
Der Unterschied VORHER/NACHHER im selben Lauf ist dagegen
|
|
stabil: Bild messen, Meldung leeren, Bild noch einmal messen. */
|
|
/* ZUERST DIE REGIE ZUMACHEN. Steht sie offen, ist das Bild auf
|
|
320 px ohnehin 0 -- dann misst die Kostenrechnung „0 -> 0" und
|
|
sagt nichts. Genau das war mein erster Anlauf. */
|
|
await seite.evaluate(() => { document.getElementById("pult").dataset.auf = "nein"; });
|
|
await seite.waitForTimeout(350);
|
|
|
|
const kosten = await (async () => {
|
|
const vorher = await seite.evaluate(() => Math.round(
|
|
document.getElementById("raum").getBoundingClientRect().height));
|
|
const text = await seite.evaluate(() => {
|
|
const f = document.getElementById("fehler");
|
|
const t = (f?.textContent || "").trim();
|
|
if (f) f.textContent = "";
|
|
return t;
|
|
});
|
|
await seite.waitForTimeout(350);
|
|
const nachher = await seite.evaluate(() => Math.round(
|
|
document.getElementById("raum").getBoundingClientRect().height));
|
|
return { vorher, nachher, text };
|
|
})();
|
|
console.log(` Hinweisfeld: ${kosten.text ? `"${kosten.text.slice(0, 44)}…"` : "leer"}`
|
|
+ ` · kostet das Bild ${kosten.nachher - kosten.vorher} px`
|
|
+ ` (${kosten.vorher} -> ${kosten.nachher})`);
|
|
|
|
/* ---- Regie zu ---------------------------------------------------- */
|
|
await seite.evaluate(() => { document.getElementById("pult").dataset.auf = "nein"; });
|
|
await seite.waitForTimeout(350);
|
|
const zu = await messen(seite);
|
|
console.log(` Regie zu : Bild ${zu.bild} px · Transport endet bei `
|
|
+ `${zu.transportUnten} von ${zu.fenster}`);
|
|
ok(zu.bild >= BILD_MIN,
|
|
`mit geschlossener Regie ist das Bild ${zu.bild} px hoch (mindestens ${BILD_MIN})`);
|
|
ok(zu.transportUnten !== null && zu.transportUnten <= zu.fenster + 1,
|
|
` und die Transportleiste endet im Fenster (${zu.transportUnten} von ${zu.fenster})`);
|
|
ok(zu.zuImFenster && zu.zuObenauf,
|
|
" der Knopf zum Auf- und Zumachen steht im Fenster und liegt obenauf");
|
|
|
|
/* ---- Regie auf --------------------------------------------------- */
|
|
await seite.evaluate(() => { document.getElementById("pult").dataset.auf = "ja"; });
|
|
await seite.waitForTimeout(350);
|
|
const auf = await messen(seite);
|
|
console.log(` Regie auf: Bild ${auf.bild} px · Transport endet bei `
|
|
+ `${auf.transportUnten} von ${auf.fenster}`);
|
|
|
|
/* DIE ZUSAGE GILT SEIT DEM 02.10.2026 AUF JEDER BREITE.
|
|
|
|
Bis dahin stand hier eine Ausnahme fuer alles unter 360 px:
|
|
Dort blieb die Regie im Raster, das Bild fiel auf 0 und die
|
|
Transportleiste rutschte 1017 Pixel unter den Fensterrand. Die
|
|
Ausnahme ist weg, weil der Mangel weg ist -- und genau deshalb
|
|
wird hier jetzt BEHAUPTET statt protokolliert. Eine Pruefung,
|
|
die eine Reparatur nicht bemerkt, hat die Reparatur nie
|
|
abgesichert. */
|
|
ok(auf.bild >= BILD_MIN,
|
|
`auch mit offener Regie bleibt das Bild ${auf.bild} px hoch`);
|
|
ok(auf.transportUnten !== null && auf.transportUnten <= auf.fenster + 1,
|
|
` und die Transportleiste bleibt im Fenster (${auf.transportUnten})`);
|
|
|
|
/* DER INHALT DER SCHUBLADE MUSS ERREICHBAR SEIN.
|
|
|
|
Eine Schublade, die ueber dem Bild liegt und deren Inhalt man
|
|
nicht erreicht, ist kein Fortschritt gegenueber der alten --
|
|
sie verdeckt dann nur eleganter. Gemessen wird deshalb, dass
|
|
der Inhalt entweder vollstaendig hineinpasst ODER rollt, und
|
|
dass die drei grossen Knoepfe ihre 44 Pixel behalten und
|
|
obenauf liegen. */
|
|
const innen = await seite.evaluate(() => {
|
|
const i = document.getElementById("pult-innen");
|
|
const p = document.getElementById("pult");
|
|
const rollt = (e) => !!e && e.scrollHeight > e.clientHeight + 2;
|
|
const knoepfe = [...document.querySelectorAll(".pult__tun .tun")].map((b) => {
|
|
const r = b.getBoundingClientRect();
|
|
const oben = document.elementFromPoint(
|
|
Math.min(innerWidth - 1, Math.max(0, r.left + r.width / 2)),
|
|
Math.min(innerHeight - 1, Math.max(0, r.top + r.height / 2)));
|
|
return {
|
|
wort: (b.textContent || "").trim().slice(0, 14),
|
|
hoch: Math.round(r.height),
|
|
/* `scrollWidth <= clientWidth` heisst: der Text ist ganz da.
|
|
Knoepfe schmaler zu machen ist erlaubt, sie abzuschneiden
|
|
nicht. */
|
|
ganz: b.scrollWidth <= b.clientWidth + 1,
|
|
erreichbar: r.top >= 0 && r.bottom <= innerHeight
|
|
&& !!oben && (oben === b || b.contains(oben) || !!oben.closest(".pult__tun")),
|
|
};
|
|
});
|
|
return {
|
|
passtOderRollt: !!i && (i.scrollHeight <= i.clientHeight + 2 || rollt(i) || rollt(p)),
|
|
sichtbar: i ? Math.round(i.getBoundingClientRect().height) : 0,
|
|
knoepfe,
|
|
};
|
|
});
|
|
ok(innen.passtOderRollt,
|
|
` der Inhalt der Schublade ist erreichbar (${innen.sichtbar} px sichtbar, Rest rollt)`);
|
|
ok(innen.knoepfe.length === 3 && innen.knoepfe.every((k) => k.hoch >= 44),
|
|
` die drei grossen Knoepfe behalten ihre 44 Pixel `
|
|
+ `(${innen.knoepfe.map((k) => k.hoch).join("/")})`);
|
|
ok(innen.knoepfe.every((k) => k.ganz),
|
|
" und keiner ist abgeschnitten — schmaler ja, beschnitten nein");
|
|
ok(innen.knoepfe.every((k) => k.erreichbar),
|
|
` alle drei sind im Fenster und liegen obenauf `
|
|
+ `(${innen.knoepfe.map((k) => k.wort).join(", ")})`);
|
|
|
|
ok(auf.zuImFenster && auf.zuObenauf,
|
|
" und der Knopf zum Zumachen ist auch jetzt da und anklickbar — "
|
|
+ "das trennt einen Schoenheitsfehler von einer Falle");
|
|
|
|
/* ==== DER HINWEIS: SICHTBAR, ABER NICHT AUF KOSTEN DES BILDES ===
|
|
|
|
Zwei Fehler an einer Stelle, beide am 02.10.2026 gemessen:
|
|
|
|
· Er war NULL PIXEL hoch, obwohl `hidden=false` und 104
|
|
Zeichen Text -- ein `role="alert"`, den niemand sieht.
|
|
· Und auf 320 px hatte er sich vorher eine stillschweigende
|
|
fuenfte Rasterzeile genommen: 71 Pixel, die dem Bild
|
|
fehlten (56 statt 127).
|
|
|
|
HIER WIRD DER ZUSTAND SELBST HERGESTELLT und nicht abgewartet.
|
|
Im Betrieb erscheint der Hinweis, wenn Kamera oder Mikrofon
|
|
fehlen -- auf einem Messrechner ist das immer so, auf einem
|
|
Handy mit Freigabe nie. Eine Pruefung, die auf die Kamera des
|
|
Messrechners baut, prueft die Messumgebung. Also: Text
|
|
hineinschreiben, sichtbar machen, messen. */
|
|
const hinweis = await seite.evaluate(() => {
|
|
const f = document.getElementById("fehler");
|
|
const bildVorher = Math.round(
|
|
document.getElementById("leinwand").getBoundingClientRect().height);
|
|
f.textContent = "Kamera oder Mikrofon sind nicht freigegeben. "
|
|
+ "Im Browser oben in der Adresszeile laesst sich das aendern.";
|
|
f.hidden = false;
|
|
return { bildVorher };
|
|
});
|
|
await seite.waitForTimeout(300);
|
|
const hinweisLage = await seite.evaluate(() => {
|
|
const f = document.getElementById("fehler");
|
|
/* ==== NOCH EINMAL SICHTBAR MACHEN -- GEGEN EINEN WETTLAUF
|
|
(08.10.2026)
|
|
|
|
Diese Pruefung meldete am 08.10. einmal „0 px" und beim
|
|
naechsten Lauf 62 px, bei unveraendertem Code. Ein Ergebnis,
|
|
das zwischen zwei Laeufen springt, ist zuerst als Wettlauf
|
|
verdaechtig und nicht als Fund -- und so war es auch:
|
|
`sagFehler()` in der Seite setzt beim Anzeigen eine Uhr, die
|
|
das Feld nach sechs Sekunden wieder versteckt. Lief vorher
|
|
irgendwo eine solche Meldung, fiel ihre Uhr in genau diese
|
|
300 Millisekunden, und gemessen wurde ein unsichtbares Feld.
|
|
|
|
Die Frage dieser Pruefung ist „ist der Hinweis lesbar, WENN er
|
|
erscheint" -- also wird er hier noch einmal aufgedeckt. Das
|
|
entfernt den Zufall, ohne die Frage zu aendern. */
|
|
f.hidden = false;
|
|
const r = f.getBoundingClientRect();
|
|
const mitte = document.elementFromPoint(
|
|
Math.min(innerWidth - 1, Math.max(0, r.left + r.width / 2)),
|
|
Math.min(innerHeight - 1, Math.max(0, r.top + r.height / 2)));
|
|
return {
|
|
hoch: Math.round(r.height),
|
|
imFenster: r.height > 0 && r.top >= 0 && r.bottom <= innerHeight,
|
|
obenauf: !!mitte && (mitte === f || f.contains(mitte)),
|
|
bildNachher: Math.round(
|
|
document.getElementById("leinwand").getBoundingClientRect().height),
|
|
};
|
|
});
|
|
ok(hinweisLage.hoch > 0 && hinweisLage.imFenster && hinweisLage.obenauf,
|
|
`der Kamerahinweis ist lesbar, wenn er erscheint (${hinweisLage.hoch} px, im Fenster`
|
|
+ `, obenauf) — vorher war er 0 px hoch`);
|
|
ok(hinweisLage.bildNachher === hinweis.bildVorher,
|
|
` und er kostet das Bild nichts (${hinweis.bildVorher} -> ${hinweisLage.bildNachher} px)`
|
|
+ " — vorher nahm er sich auf 320 px eine eigene Rasterzeile");
|
|
await seite.evaluate(() => {
|
|
const f = document.getElementById("fehler");
|
|
f.textContent = ""; f.hidden = true;
|
|
});
|
|
|
|
ok(konsole.length === 0,
|
|
konsole.length ? "Fehler im Browser: " + konsole.join(" | ") : "kein Absturz im Browser");
|
|
|
|
/* ---- GEGENPROBE: kann diese Messung ueberhaupt rot werden? --------
|
|
Nur einmal, am schmalsten Geraet. Beide Behauptungen werden
|
|
absichtlich gebrochen und muessen dann durchfallen. */
|
|
if (breite === 320) {
|
|
await seite.evaluate(() => {
|
|
document.getElementById("pult").dataset.auf = "nein";
|
|
document.getElementById("leinwand").style.height = "0px";
|
|
});
|
|
await seite.waitForTimeout(250);
|
|
const kaputt = await messen(seite);
|
|
ok(kaputt.bild < BILD_MIN,
|
|
`Gegenprobe: ein auf 0 gedruecktes Bild faellt durch (${kaputt.bild} px)`);
|
|
|
|
await seite.evaluate(() => {
|
|
document.getElementById("leinwand").style.height = "";
|
|
const deckel = document.createElement("div");
|
|
deckel.id = "deckel-probe";
|
|
deckel.style.cssText = "position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.01)";
|
|
document.body.append(deckel);
|
|
});
|
|
await seite.waitForTimeout(250);
|
|
const verdeckt = await messen(seite);
|
|
ok(verdeckt.zuImFenster && !verdeckt.zuObenauf,
|
|
" und ein zugedeckter Knopf gilt nicht als anklickbar, obwohl er im Fenster steht");
|
|
await seite.evaluate(() => document.getElementById("deckel-probe")?.remove());
|
|
}
|
|
|
|
await kontext.close();
|
|
}
|
|
|
|
await browser.close();
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
console.log(`\n${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
|
|
process.exit(fehler === 0 ? 0 : 1);
|