DER BEFUND
Im naechtlichen Lauf standen dreizehn Pruefungen mit derselben Zeile
rot:
[uncaughtException] Error: spawnSync openssl ENOENT
Kein Programmfehler. Die Pruefungen brauchen einen https-Vorbau (der
Sitzungskeks des Hauses ist `secure` und kommt ueber http nicht an),
und dafuer erzeugen sie ein Wegwerf-Zertifikat mit openssl.
GEMESSEN, NICHT VERMUTET
openssl liegt hier: C:\Program Files\Git\mingw64\bin\openssl.exe
C:\Program Files\Git\usr\bin\openssl.exe
im System-PATH steht: C:\Program Files\Git\cmd (nur git.exe)
im Benutzer-PATH: nichts mit Git
Wer aus Git Bash startet, erbt die mingw-Pfade und merkt nie etwas.
Die Aufgabenplanung startet `node.exe` DIREKT, ohne Shell -- und
bekommt sie nicht. Bei mir gruen, nachts rot, und der Grund steht
nicht im Code.
Das Schlimmste daran ist nicht der Ausfall: Dreizehn dauerhaft rote
Zeilen in einer Notiz, die Filipe morgens liest, gewoehnen einem das
Hinsehen ab -- und decken dabei die echten Befunde zu.
DIE REPARATUR
`server/helfer-openssl.mjs` sucht openssl erst im PATH (ohne `where`
oder `which` -- beides sind selbst Programme und koennen genauso
fehlen), danach an den Orten, an denen es auf einem Windows-Rechner
mit Git wirklich liegt.
Dazu `zertifikatBauen(schluessel, zertifikat, host)`. Die acht
Aufrufformen im Haus waren identisch bis auf die Variablennamen
(schl/zert, schluesselDatei/zertDatei, zKey/zCrt, sk/zt, key/crt) --
eine Stelle traegt alle neunundzwanzig. Zwei Tage zuvor war eine
davon um ein `-addext` aermer als die anderen; gemerkt hat es
niemand, weil der Browser den fehlenden Alternativnamen erst bei
einer Weiterleitung anmahnt. Eine Stelle kann nicht von sich selbst
abweichen.
KEIN EINTRAG IN DEN SYSTEM-PATH. `mingw64\bin` enthaelt rund hundert
Programme mit Unix-Namen (`find`, `sort`, `link`), die gleichnamige
Windows-Befehle verdecken. Das fuer eine Pruefung zu aendern haette
an ganz anderen Stellen Fehler verursacht, die niemand hierher
zurueckverfolgt.
GEGENPROBE IN BEIDE RICHTUNGEN, mit dem PATH des Nachtlaufs:
vorher (direkter Aufruf): ABSTURZ: spawnSync openssl ENOENT
nachher (ueber den Helfer): Zertifikat gebaut, 1236 Bytes
Und eine echte Pruefung unter denselben Bedingungen:
PATH ohne mingw64 -> pruef-chat-neu-stelle
63 Pruefungen, 0 Fehler, 0 Abstuerze
Genau diese Datei stand im Nachtlauf mit ENOENT rot.
EINE WACHE DAGEGEN
pruef-struktur prueft ab jetzt, dass niemand openssl wieder direkt
ruft -- der naechste merkt es dort und nicht erst in einem
Nachtlauf, den niemand liest. Drei Gegenproben.
Beim ersten Anlauf schlug sie auf ihre EIGENEN Probetexte an: Sie
liest alle Serverdateien, und dazu gehoert sie selbst. Die Texte
werden jetzt zusammengesetzt. Derselbe Selbsttreffer ist mir heute
schon zweimal passiert.
DER DRITTE AUSGANG BLEIBT, WO ER HINGEHOERT
`helfer-kachel-echtfarbe.mjs` faengt den Fehler weiterhin ab und
meldet `moeglich: false` mit Grund, statt abzubrechen. Eine
Sicherung abzuschaffen, weil ihr Anlass gerade behoben ist, ist der
Anfang des naechsten stillen Fehlschlags.
NEBENBEI: pruef-community-sicht
Sie war rot mit "ZU VIEL: reaktion.html" -- mein eigener Rueckstand
vom 28.09. Die Reaction steht als Kachel im Community-Bereich; die
Liste war nicht nachgezogen. Jetzt 10 / 0.
Die Liste bleibt bewusst von Hand gepflegt: Sie ist eine ABSICHT,
keine Ableitung. Aus rechte.js gelesen verglichen sich zwei Kopien
derselben Quelle -- immer gruen, nie ein Beweis.
GEPRUEFT
pruef-struktur ALLES IN ORDNUNG (mit der neuen Wache)
pruef-community-sicht 10 / 0
pruef-chat-neu-stelle 63 / 0 (mit dem PATH des Nachtlaufs)
pruef-notizen, -teamlage-karten, -werdegang: EXIT 0, keine Abstuerze
mess-quer EXIT 0
29 Dateien: node --check auf allen, kein direkter Aufruf mehr
875 lines
42 KiB
JavaScript
875 lines
42 KiB
JavaScript
/* =====================================================================
|
||
pruef-handy-teamdogi.mjs — Der Rundgang, den es für diese Rollen
|
||
noch nie gab. (19.09.2026)
|
||
|
||
Filipe: „mach einen kompletten check dass die app auf dem handy
|
||
perfekt funktioniert … weil die modis haben schon probleme."
|
||
Auf die Rückfrage, was genau: „einfach ALLES ABCHECKEN ALLES
|
||
MÖGLICHE."
|
||
|
||
---------------------------------------------------------------------
|
||
DER BEFUND, DER DIESE DATEI AUSGELÖST HAT
|
||
|
||
Es gibt seit dem 02.09.2026 einen großen Rundgang (`pruef-grosscheck`).
|
||
Er geht über jede Seite, zwei Bildschirmgrößen und vier Rollen:
|
||
|
||
admin · manager · scout · creator
|
||
|
||
Vier von acht. Es fehlen **modi, hand und gast** — also genau die drei
|
||
Rollen, die auf `crew.dogfather-universe.com` leben, und genau die,
|
||
von denen die Beschwerden kommen. Ihre Seiten sind nie im Ganzen auf
|
||
einem Handy durchgemessen worden.
|
||
|
||
Das ist kein Vorwurf an den Rundgang: Er wurde für das Agenturhaus
|
||
gebaut, und das Team-Dogi-Haus kam später dazu. Aber es erklärt,
|
||
warum Fehler dort überleben konnten.
|
||
|
||
---------------------------------------------------------------------
|
||
WARUM DIE PRÜFUNG WIRKLICH `crew.` AUFRUFT
|
||
|
||
Das Haus entscheidet am **Host**, wer was sieht:
|
||
`haus = istCrewAdresse(req.get("host")) ? "crew" : …`. Gegen
|
||
`127.0.0.1` gemessen bekäme man ein anderes Haus, andere Kacheln und
|
||
eine Adressregel, die alles durchlässt — und damit ein Ergebnis über
|
||
eine Seite, die es so nicht gibt.
|
||
|
||
Deshalb bekommt der Browser `--host-resolver-rules`, die
|
||
`crew.dogfather-universe.com` auf den lokalen Server zeigen lassen.
|
||
Die Anwendung sieht den echten Namen, die Prüfung trifft trotzdem
|
||
niemals den echten Server.
|
||
|
||
---------------------------------------------------------------------
|
||
ZWEI BREITEN, UND BEIDE HABEN EINEN GRUND
|
||
|
||
390 px iPhone 12/13/14/15 — die schmalste verbreitete Breite
|
||
412 px die häufigste Android-Breite überhaupt
|
||
|
||
412 ist der wichtigere Wert, und das ist gemessen, nicht geraten: Im
|
||
Haus sind schon zweimal Umbrüche für „das Handy" auf 380 px gerechnet
|
||
worden. Bei 390 ging es gut, bei 412 lag ein Knopf über dem anderen —
|
||
und beim Rundgang fiel es nicht auf, weil er nur 390 kannte.
|
||
|
||
---------------------------------------------------------------------
|
||
WAS GEMESSEN WIRD
|
||
|
||
1. KOMMT DIE SEITE AN? Statuscode, Inhalt, keine Umleitung ins
|
||
Leere.
|
||
2. STÜRZT ETWAS AB? Jede Fehlermeldung der Konsole und jeder
|
||
nicht abgefangene Fehler. Ein Skript,
|
||
das aussteigt, lässt alles Folgende
|
||
tot zurück — das sieht aus wie „die
|
||
Seite ist leer".
|
||
3. LÄUFT ETWAS ÜBER? Waagerechter Überstand, und WER ihn
|
||
verursacht.
|
||
4. IST TEXT ABGESCHNITTEN? Mit den Ausnahmen, die Absicht sind
|
||
(Wasserzeichen, Klassen nur für
|
||
Vorleseprogramme).
|
||
5. KANN MAN ES TREFFEN? Jedes Bedienelement unter 44 px — mit
|
||
der Ausnahme, die im Standard steht
|
||
(Links mitten im Fließtext).
|
||
6. LIEGT ETWAS ÜBEREINANDER? Zwei Bedienelemente, die sich
|
||
überdecken. Das ist der Fehler, bei dem
|
||
man auf A tippt und B bekommt — und
|
||
genau der, den Nutzer als „der Knopf
|
||
geht nicht" melden.
|
||
7. WEISS MAN, WAS ES TUT? Ein Bedienelement ohne lesbaren Namen
|
||
ist für ein Vorleseprogramm ein leerer
|
||
Knopf.
|
||
|
||
---------------------------------------------------------------------
|
||
UND EINE GEGENPROBE
|
||
|
||
Am Ende wird eine absichtlich zu breite Kachel eingebaut und ein
|
||
absichtlich zu kleiner Knopf. Beide MÜSSEN gemeldet werden. Eine
|
||
Prüfung, die immer bestätigt, bestätigt nichts — das ist hier seit
|
||
dem 01.09.2026 Hausregel.
|
||
|
||
---------------------------------------------------------------------
|
||
DIE SEITENLISTE WIRD GERECHNET, NICHT GEPFLEGT
|
||
|
||
Welche Seite zu welcher Rolle gehört, steht in `rechte.js` und
|
||
`workspace.js`. Diese Prüfung liest beides und leitet ihre Liste
|
||
daraus ab. Eine abgeschriebene Liste wäre die Stelle, an der in drei
|
||
Monaten eine neue Seite fehlt — und niemand würde es merken, weil
|
||
der Lauf ja grün bleibt.
|
||
|
||
AUFRUF: node server/pruef-handy-teamdogi.mjs
|
||
Dauer: rund drei Minuten (4 Rollen × 2 Breiten × ~20 Seiten).
|
||
===================================================================== */
|
||
|
||
import { tagLokal } from "./helfer-zeit.mjs";
|
||
import { mkdtempSync, rmSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join, dirname } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import { notbremse } from "./helfer-notbremse.mjs";
|
||
import { eigenerPort } from "./helfer-port.mjs";
|
||
import { zertifikatBauen } from "./helfer-openssl.mjs";
|
||
|
||
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-handy-"));
|
||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||
|
||
const PORT = await eigenerPort(import.meta, "pruef-handy-teamdogi");
|
||
process.env.PORT = String(PORT);
|
||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||
|
||
/* Der Server setzt seine Kekse mit `secure` -- über http bliebe die
|
||
Anmeldung damit wirkungslos, und ALLES weitere wäre eine Messung an
|
||
der Anmeldewand. */
|
||
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");
|
||
/* NACH index.js: Der Server hängt dort zwei Auffangnetze ein, die einen
|
||
Absturz nur protokollieren. Richtig für den Betrieb, fatal für eine
|
||
Prüfung -- ohne die Notbremse bliebe der Prozess ewig stehen. */
|
||
notbremse(600_000, "pruef-handy-teamdogi");
|
||
await new Promise((r) => setTimeout(r, 900));
|
||
|
||
/* ---------------------------------------------------------------------
|
||
Die Leute, über die geprüft wird
|
||
------------------------------------------------------------------- */
|
||
const { DatabaseSync } = await import("node:sqlite");
|
||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||
|
||
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 jetzt = new Date().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, aktiv, erstellt)"
|
||
+ " VALUES (?,?,?,?,?,1,?)").run(name, rolle, hash, salt, 32768, jetzt);
|
||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
}
|
||
|
||
/* LANGE NAMEN MIT ABSICHT. Ein „Max" passt überall hin; ein Name, der
|
||
wirklich vorkommt, findet die Stellen, an denen zu wenig Platz ist.
|
||
Dasselbe macht der große Rundgang. */
|
||
const idDogi = anlegen("Filipe Fernandes Ribeiro", "admin", "CODE-DOGI-0001");
|
||
const idHand = anlegen("VanVan Schmidt-Hohenberg", "hand", "CODE-HAND-0001");
|
||
const idModi = anlegen("Diene Margarethe Oberländer", "modi", "CODE-MODI-0001");
|
||
const idGast = anlegen("Melli aus der Community", "gast", "CODE-GAST-0001");
|
||
|
||
/* Die Community muss ihr Alter bestätigt haben -- sonst hängt die
|
||
Anmeldung an der Altersfrage, und wir messen die Wand statt der App. */
|
||
try {
|
||
d.prepare("UPDATE personen SET alter_bestaetigt_am = ? WHERE id = ?").run(jetzt, idGast);
|
||
} catch { /* Spalte gibt es (noch) nicht -- dann klickt die Prüfung die Frage an. */ }
|
||
|
||
/* ---- Inhalt, damit die Seiten nicht leer sind ----------------------
|
||
Leere Seiten prüfen nichts: Ein Raster ohne Karten läuft nie über,
|
||
und ein Knopf, der nie gezeichnet wird, ist nie zu klein. */
|
||
const setze = (sql, ...w) => { try { d.prepare(sql).run(...w); } catch { /* Tabelle fehlt */ } };
|
||
|
||
for (const [titel, wer] of [
|
||
["Thumbnail für den Freitagsstream bauen und hochladen", idModi],
|
||
["Regelverstoß von gestern Abend nachbereiten", idModi],
|
||
["Community-Umfrage auswerten", idHand],
|
||
]) {
|
||
setze("INSERT INTO aufgaben (titel, beschreibung, status, verantwortlich_id, erstellt_von,"
|
||
+ " erstellt, frist) VALUES (?,?,?,?,?,?,?)",
|
||
titel, "Beschreibung mit genug Text, dass die Karte wirklich Höhe bekommt "
|
||
+ "und ein Umbruch entstehen kann.", "offen", wer, idDogi, jetzt,
|
||
/* Ortszeit, nicht UTC: Zwischen 00:00 und 02:00 deutscher Zeit
|
||
liegt UTC noch im Vortag -- die Frist waere dann um einen Tag
|
||
verschoben, und die Pruefung schluege nachts falsch an.
|
||
Gefunden hat das `pruef-struktur`, nicht ich. */
|
||
tagLokal(3));
|
||
}
|
||
|
||
for (const [bereich, titel] of [
|
||
["treff", "Hallo zusammen, ich bin neu hier und freu mich auf die Streams"],
|
||
["anschlag", "Ab Montag starten wir eine Stunde früher"],
|
||
["wunsch", "Zockernacht am Samstag mit der ganzen Community"],
|
||
["ansteht", "Stream am Freitag um 20 Uhr"],
|
||
["highlight", "Der Moment, als Dogi den Boss gelegt hat"],
|
||
["mitmachen", "Wie werde ich Modi?"],
|
||
["rueckmeldung", "Die neue Aufgabenübersicht ist viel klarer geworden"],
|
||
]) {
|
||
setze("INSERT INTO eintraege (bereich, titel, text, status, erstellt_von, erstellt)"
|
||
+ " VALUES (?,?,?,?,?,?)", bereich, titel,
|
||
"Ein Text, der lang genug ist, dass er über mehrere Zeilen läuft und damit "
|
||
+ "zeigt, ob die Karte auf einem schmalen Bildschirm sauber umbricht oder nicht.",
|
||
"offen", idDogi, jetzt);
|
||
}
|
||
|
||
setze("INSERT INTO wissen (titel, kategorie, text, erstellt_von, erstellt)"
|
||
+ " VALUES (?,?,?,?,?)", "Wie du einen Stream vorbereitest", "Live",
|
||
"Schritt für Schritt erklärt.", idDogi, jetzt);
|
||
|
||
d.close();
|
||
|
||
/* ---------------------------------------------------------------------
|
||
Welche Seite gehört zu welcher Rolle? — gerechnet, nicht gepflegt
|
||
------------------------------------------------------------------- */
|
||
const rechte = await import("./rechte.js");
|
||
const ws = await import("./workspace.js");
|
||
|
||
const CREW = "crew.dogfather-universe.com";
|
||
|
||
/* =====================================================================
|
||
WARUM DIESE PRÜFUNG ÜBER https SPRICHT — 19.09.2026
|
||
|
||
Der erste Anlauf lief über `http://crew.dogfather-universe.com:PORT`.
|
||
Ergebnis: JEDE Stilvorlage, jedes Bild und jedes Skript schlug mit
|
||
`ERR_SSL_PROTOCOL_ERROR` fehl. Gemessen wurde also eine Seite ganz
|
||
ohne CSS — und die meldete brav ein 1647 px breites Bild auf einem
|
||
390 px breiten Schirm.
|
||
|
||
Das sah aus wie ein schwerer Fund und war einer in MEINER Prüfung:
|
||
`dogfather-universe.com` steht in der fest eingebauten HSTS-Liste von
|
||
Chromium. Der Browser schaltet dort von sich aus auf https um, und
|
||
zwar unabänderlich — auch `--disable-features=HttpsUpgrades` hilft
|
||
nicht, das ist nachgemessen.
|
||
|
||
Eine plausible Herleitung ersetzt eben keine Messung. Hätte ich den
|
||
Befund gemeldet statt nachzusehen, wäre ein halber Tag in eine
|
||
Reparatur geflossen, die nichts repariert.
|
||
|
||
Deshalb spricht die Prüfung jetzt selbst https: ein eigenes
|
||
Zertifikat für genau diesen Namen, ein winziger Vorbau davor, der
|
||
alles unverändert an den echten Server weiterreicht. Die Anwendung
|
||
merkt davon nichts — sie sieht denselben Host wie im Betrieb.
|
||
===================================================================== */
|
||
const { execFileSync } = await import("node:child_process");
|
||
const { createServer: httpsServer } = await import("node:https");
|
||
const { request: httpAnfrage } = await import("node:http");
|
||
|
||
const schluessel = join(ordner, "pruef.key");
|
||
const zertifikat = join(ordner, "pruef.crt");
|
||
zertifikatBauen(schluessel, zertifikat, CREW);
|
||
|
||
const HTTPS_PORT = PORT + 1;
|
||
const vorbau = httpsServer({
|
||
key: (await import("node:fs")).readFileSync(schluessel),
|
||
cert: (await import("node:fs")).readFileSync(zertifikat),
|
||
}, (von, nach) => {
|
||
/* UNVERÄNDERT WEITERREICHEN -- besonders die Host-Kopfzeile. An ihr
|
||
entscheidet das Haus, wer was sieht; würde sie hier umgeschrieben,
|
||
misst die Prüfung wieder das falsche Haus. */
|
||
const weiter = httpAnfrage({
|
||
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}`;
|
||
|
||
function seitenFuer(rolle) {
|
||
const tafel = rechte.tafelJetzt();
|
||
const person = { rolle, haus: "crew" };
|
||
const seiten = Object.entries(tafel)
|
||
.filter(([, liste]) => Array.isArray(liste) && liste.includes(rolle))
|
||
.map(([pfad]) => pfad)
|
||
.filter((pfad) => ws.gehoertAufDieseAdresse(person, pfad));
|
||
|
||
/* Die Bretter hinter `bereich.html?b=…` sind eigene Seiten, auch wenn
|
||
sie sich eine Datei teilen. Wer nur `bereich.html` prüft, prüft
|
||
eines von vierzehn. Welche es gibt, steht in den Kacheln. */
|
||
const bretter = [...(ws.bereicheFuer(person) || []),
|
||
...(ws.zusatzBereicheFuer(person) || [])]
|
||
.map((k) => String(k?.ziel || ""))
|
||
.filter((z) => z.startsWith("bereich.html?b="))
|
||
.map((z) => "/workspace/" + z);
|
||
|
||
return [...new Set([...seiten.filter((p) => !p.endsWith("bereich.html")), ...bretter])];
|
||
}
|
||
|
||
/* ---------------------------------------------------------------------
|
||
Die Messung — sie läuft IM Browser
|
||
------------------------------------------------------------------- */
|
||
const messen = () => {
|
||
const sichtbar = (e) => {
|
||
const r = e.getBoundingClientRect();
|
||
const st = getComputedStyle(e);
|
||
return r.width > 0 && r.height > 0 && st.visibility !== "hidden"
|
||
&& st.display !== "none" && Number(st.opacity) > 0.05;
|
||
};
|
||
const name = (e) => (e.tagName.toLowerCase()
|
||
+ (e.id ? "#" + e.id : "." + String(e.className || "").split(" ")[0])).slice(0, 36);
|
||
|
||
const BEDIENBAR = 'a[href], button, input, select, textarea, [role="button"],'
|
||
+ ' [role="tab"], [role="radio"], [tabindex]:not([tabindex="-1"])';
|
||
|
||
/* NUR-FUER-VORLESEPROGRAMME IST KEIN FEHLER. Solche Elemente sind
|
||
absichtlich 1 px groß und weggeschnitten -- sie als „zu klein" zu
|
||
melden wäre ein Fehlalarm, der die echten Funde zudeckt. */
|
||
/* WER IN EINEM ABGEKLEMMTEN KASTEN SITZT, IST SELBST ABGEKLEMMT
|
||
(geschaerft 29.09.2026).
|
||
|
||
Gemeldet wurde `a.zurueck „Team Dogi…" 0 statt 82px` auf
|
||
notizen.html. Das ist kein Mangel, sondern Absicht: Auf schmalen
|
||
Schirmen setzt start.css `.kopfleiste .marke__text` auf 1x1
|
||
Pixel mit `clip-path: inset(50%)` -- das uebliche Muster fuer
|
||
„bleibt fuer Vorleseprogramme, verschwindet fuer das Auge".
|
||
Daneben steht der Grund: Der Text steht auf jeder Seite zwei
|
||
Zeilen tiefer noch einmal als Ueberschrift.
|
||
|
||
Die Erkennung sah nur das Element SELBST an. Der Link darin ist
|
||
0 Pixel breit, aber eine Zeile hoch -- also fiel er durch beide
|
||
Bedingungen und wurde gemeldet. Sie steigt jetzt die Kette der
|
||
Vorfahren hoch.
|
||
|
||
DAS MACHT DIE REGEL SCHAERFER, NICHT LAXER: Ausgenommen wird
|
||
nur, was in einem nachweislich abgeklemmten Kasten sitzt. Ein
|
||
Name, der in einer normalen Kopfzeile auf null gequetscht wird
|
||
-- der echte Fund vom 20.09. -- hat keinen solchen Vorfahren
|
||
und wird weiter gemeldet. */
|
||
const abgeklemmt = (e) => {
|
||
const st = getComputedStyle(e);
|
||
const r = e.getBoundingClientRect();
|
||
return (r.width <= 2 && r.height <= 2)
|
||
|| st.clipPath === "inset(50%)" || st.clip === "rect(0px, 0px, 0px, 0px)";
|
||
};
|
||
const nurVorlesen = (e) => {
|
||
for (let k = e; k && k !== document.body; k = k.parentElement) {
|
||
if (abgeklemmt(k)) return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
const alle = [...document.querySelectorAll("body *")];
|
||
const bedienbar = [...document.querySelectorAll(BEDIENBAR)]
|
||
.filter(sichtbar).filter((e) => !nurVorlesen(e));
|
||
|
||
/* ---- Überstand: wer ragt über den rechten Rand? ------------------ */
|
||
const breite = document.documentElement.clientWidth;
|
||
const ueberstand = alle.filter((e) => {
|
||
if (!sichtbar(e)) return false;
|
||
const r = e.getBoundingClientRect();
|
||
/* ZIERRAT DARF HINAUSRAGEN (geschärft 19.09.2026).
|
||
|
||
Der erste Lauf meldete 26 Überstände -- und beim Nachsehen waren
|
||
es fast durchweg Dinge, die ein Mensch weder sieht noch bedient:
|
||
`div.aussen__schein` (ein Lichtschein, `aria-hidden="true"`),
|
||
`span.logo-feld__stueck` (leere Spans, aus denen ein Logo
|
||
zusammengesetzt wird), dekorative SVG. Alle werden von
|
||
`overflow: hidden` sauber weggeschnitten.
|
||
|
||
Ein Überstand ist ein Fehler, wenn er etwas WEGSCHNEIDET, das
|
||
jemand lesen oder antippen wollte. Bei einem Lichtschein ist er
|
||
die Gestaltung. Gemeldet wird deshalb nur noch, was Text trägt
|
||
oder bedienbar ist -- sonst hätte die Liste 26 Einträge, von
|
||
denen keiner stimmt, und die echten gingen darin unter. */
|
||
if (e.closest(".kopf-wasserzeichen")) return false;
|
||
if (e.closest('[aria-hidden="true"]')) return false;
|
||
const hatText = (e.textContent || "").trim().length > 0;
|
||
const bedienbarHier = e.matches(BEDIENBAR) || !!e.closest(BEDIENBAR);
|
||
if (!hatText && !bedienbarHier) return false;
|
||
|
||
/* WAS IN EINEM ROLLENDEN KASTEN STEHT, LAEUFT NICHT UEBER
|
||
(geschaerft 22.09.2026).
|
||
|
||
Der Rundgang meldete drei Befunde, bei jeder Rolle dieselben:
|
||
`button.schnitt bis 481px` auf aufgaben.html und die
|
||
Rechtetafel `table bis 877px`. Nachgemessen bei 390 px:
|
||
Das Dokument ist exakt 390 px breit, NICHTS laeuft ueber --
|
||
beide stehen in einem Kasten mit `overflow-x: auto`, und der
|
||
rollt absichtlich waagerecht. Eine breite Tabelle auf einem
|
||
schmalen Schirm ist genau so geloest, wie man es loest.
|
||
|
||
Diese drei Meldungen kamen bei JEDEM Lauf. Und eine Warnung,
|
||
die immer kommt, ist keine Warnung mehr -- man klickt sie weg
|
||
und uebersieht ab da auch die echte. (Hausregel vom 03.09.2026,
|
||
damals zu einer Loeschwarnung, die bei jedem Aufruf ansprang.)
|
||
|
||
DIE REGEL IST SCHMAL: Es zaehlt nur, wenn ein Vorfahre
|
||
ausdruecklich `overflow-x: auto` oder `scroll` traegt. Ein
|
||
Element, das einfach so hinausragt, wird weiter gemeldet --
|
||
und `overflow: hidden` gilt NICHT, denn dort ist der Inhalt
|
||
wirklich weg und nicht erreichbar. */
|
||
for (let v = e.parentElement; v && v !== document.body; v = v.parentElement) {
|
||
const ox = getComputedStyle(v).overflowX;
|
||
if (ox === "auto" || ox === "scroll") return false;
|
||
}
|
||
return r.right > breite + 2;
|
||
}).slice(0, 4).map((e) => `${name(e)} bis ${Math.round(e.getBoundingClientRect().right)}px`);
|
||
|
||
/* ---- Abgeschnittener Text ---------------------------------------- */
|
||
const abgeschnitten = alle.filter((e) => {
|
||
if (!sichtbar(e) || e.children.length > 0) return false;
|
||
/* NUR-FUER-VORLESEPROGRAMME IST KEIN ABGESCHNITTENER TEXT
|
||
(nachgetragen 19.09.2026).
|
||
|
||
Der erste Lauf meldete 192-mal `span.zurueck-knopf__text` und
|
||
154-mal `span.teilen__text` als abgeschnitten. Beide sind
|
||
ABSICHTLICH 1 px gross und weggeschnitten: Auf dem Handy sagt
|
||
der Pfeil allein „zurueck", das Wort daneben kostet 65 px --
|
||
und ein Vorleseprogramm liest es trotzdem.
|
||
|
||
Gemeldet als Fehler waeren das rund 350 Fehlalarme, die jeden
|
||
echten Fund zudecken. Eine Warnung, die immer kommt, ist keine
|
||
Warnung mehr -- diese Lehre steht seit dem 03.09.2026 im Haus,
|
||
und sie gilt fuer meine eigene Pruefung genauso. */
|
||
if (nurVorlesen(e)) return false;
|
||
const t = (e.textContent || "").trim();
|
||
if (t.length < 4) return false;
|
||
const st = getComputedStyle(e);
|
||
if (st.overflow === "visible" && st.textOverflow !== "ellipsis") return false;
|
||
/* `text-overflow: ellipsis` ist eine ABSICHT, keine Panne -- dort
|
||
ist das Kürzen gewollt und die volle Fassung steht im title. */
|
||
if (st.textOverflow === "ellipsis") return false;
|
||
/* `-webkit-line-clamp` IST AUCH ABSICHT (nachgetragen 19.09.2026).
|
||
Eine Beschreibung, die nach zwei Zeilen endet, ist gewollt --
|
||
die volle Fassung steht beim Aufklappen. Ohne diese Zeile meldete
|
||
die Pruefung `.kachel__beschreibung` 24-mal als abgeschnitten. */
|
||
if (st.webkitLineClamp && st.webkitLineClamp !== "none") return false;
|
||
return e.scrollWidth > e.clientWidth + 3 || e.scrollHeight > e.clientHeight + 3;
|
||
}).slice(0, 4).map((e) => `${name(e)} „${(e.textContent || "").trim().slice(0, 28)}…"`);
|
||
|
||
/* ---- Auf fast nichts zusammengedrückter Text (20.09.2026) --------
|
||
|
||
ZWEI EIGENE ENTSCHEIDUNGEN ERGABEN ZUSAMMEN EIN LOCH, und es hat
|
||
einen echten Fehler durchgelassen: In der Kopfzeile eines
|
||
Gesprächs blieben vom Namen 0 bis 33 px übrig — auf JEDEM
|
||
Telefon. Man öffnete einen Chat und sah nicht, mit wem man
|
||
schreibt.
|
||
|
||
Warum hier nichts gemeldet wurde:
|
||
|
||
1. `sichtbar()` verlangt `width > 0`. Ein auf null gequetschtes
|
||
Element ist unsichtbar -- und "unsichtbar" hiess "nichts zu
|
||
prüfen". Genau falsch herum: Nichts zu sehen IST der Befund.
|
||
2. `text-overflow: ellipsis` gilt als Absicht. Das stimmt auch
|
||
-- ein langer Name soll gekürzt werden. Es stimmt nur nicht
|
||
mehr, wenn nichts übrig bleibt.
|
||
|
||
DIE REGEL IST BEWUSST SCHMAL. "Jede Kürzung melden" wären die
|
||
~350 Fehlalarme, die weiter oben schon einmal alles zugedeckt
|
||
haben. Gemeldet wird nur, was UNTER 40 px sichtbar ist und
|
||
mindestens das Doppelte bräuchte -- da steht kein Wort mehr,
|
||
sondern ein Buchstabe mit Punkten. */
|
||
const zerquetscht = alle.filter((e) => {
|
||
if (e.children.length > 0) return false;
|
||
if (nurVorlesen(e)) return false;
|
||
const t = (e.textContent || "").trim();
|
||
if (t.length < 4) return false;
|
||
const st = getComputedStyle(e);
|
||
if (st.visibility === "hidden" || st.display === "none") return false;
|
||
if (Number(st.opacity) <= 0.05) return false;
|
||
/* Steht es überhaupt im Aufbau? Ein Element in einem geschlossenen
|
||
Klappfeld ist kein Fall -- es ist gar nicht dran. */
|
||
if (!e.offsetParent && st.position !== "fixed") return false;
|
||
const breiteHier = e.getBoundingClientRect().width;
|
||
const noetig = e.scrollWidth;
|
||
return breiteHier < 40 && noetig >= breiteHier * 2 && noetig > 60;
|
||
}).slice(0, 4).map((e) => `${name(e)} „${(e.textContent || "").trim().slice(0, 24)}…" `
|
||
+ `${Math.round(e.getBoundingClientRect().width)} statt ${Math.round(e.scrollWidth)}px`);
|
||
|
||
/* ---- Zu kleine Tippziele ----------------------------------------- */
|
||
const klein = bedienbar.filter((e) => {
|
||
/* WAS KEINE KLICKS ANNIMMT, IST KEIN TIPPZIEL
|
||
(geschaerft 22.09.2026).
|
||
|
||
Gemeldet wurde `a.k-pille 8x8` auf kalender.html -- die Punkte,
|
||
die im Monatsraster am Handy fuer je einen Termin stehen. Sie
|
||
sind ABSICHTLICH 8 px klein und tragen `pointer-events: none`:
|
||
Der Tipp gehoert der ganzen Tageszelle, nicht dem Punkt. Ein
|
||
8-px-Ziel waere ein Nadeloehr; die Zelle daneben ist 46 px und
|
||
oeffnet den Tag.
|
||
|
||
Wer keine Klicks annimmt, kann auch kein zu kleines Ziel sein.
|
||
Die Regel ist damit schaerfer, nicht laxer: Ein Knopf, der
|
||
Klicks annimmt und zu klein ist, wird weiter gemeldet -- die
|
||
Gegenprobe weiter unten weist genau das nach. */
|
||
if (getComputedStyle(e).pointerEvents === "none") return false;
|
||
const r = e.getBoundingClientRect();
|
||
/* 43,5 STATT 44: Ein Knopf von 43,98 px wird als „44x44"
|
||
angezeigt und waere trotzdem ein Fund -- ein halbes Pixel
|
||
aus einer Zeilenhoehe ist kein Bedienproblem, aber es kostet
|
||
Vertrauen in die Liste. */
|
||
if (r.width >= 43.5 && r.height >= 43.5) return false;
|
||
/* DIE AUSNAHME AUS DEM STANDARD (WCAG 2.5.8): Ein Link mitten in
|
||
einem Satz darf klein sein -- ihn zu vergrößern würde den Satz
|
||
zerreißen. Erkannt daran, dass der Elternteil deutlich mehr Text
|
||
trägt als der Link selbst. */
|
||
const p = e.parentElement;
|
||
const eigen = (e.textContent || "").trim().length;
|
||
const drumherum = ((p?.textContent || "").trim().length) - eigen;
|
||
if (e.tagName === "A" && drumherum > eigen + 20) return false;
|
||
return true;
|
||
}).slice(0, 5).map((e) => {
|
||
const r = e.getBoundingClientRect();
|
||
return `${name(e)} ${Math.round(r.width)}×${Math.round(r.height)}`;
|
||
});
|
||
|
||
/* ---- Überlappende Bedienelemente ---------------------------------
|
||
DER WICHTIGSTE PUNKT. Wer auf A tippt und B bekommt, meldet „der
|
||
Knopf geht nicht" -- und niemand findet etwas, weil beide Knöpfe
|
||
für sich genommen tadellos sind. Genau dieser Fehler ist im Haus
|
||
schon zweimal aufgetreten. */
|
||
const ueberdeckt = [];
|
||
for (let i = 0; i < bedienbar.length && ueberdeckt.length < 4; i++) {
|
||
for (let j = i + 1; j < bedienbar.length; j++) {
|
||
const a = bedienbar[i], b = bedienbar[j];
|
||
if (a.contains(b) || b.contains(a)) continue;
|
||
const ra = a.getBoundingClientRect(), rb = b.getBoundingClientRect();
|
||
const breiteUeber = Math.min(ra.right, rb.right) - Math.max(ra.left, rb.left);
|
||
const hoeheUeber = Math.min(ra.bottom, rb.bottom) - Math.max(ra.top, rb.top);
|
||
if (breiteUeber <= 6 || hoeheUeber <= 6) continue;
|
||
/* Ein Element in einem geöffneten Dialog darf den Rest
|
||
überdecken -- das ist der Zweck eines Dialogs. */
|
||
if (a.closest("dialog[open], .anruf, .anruf-klingelt")
|
||
!== b.closest("dialog[open], .anruf, .anruf-klingelt")) continue;
|
||
ueberdeckt.push(`${name(a)} ⨯ ${name(b)} (${Math.round(breiteUeber)}×${Math.round(hoeheUeber)}px)`);
|
||
break;
|
||
}
|
||
}
|
||
|
||
/* ---- Bedienelemente ohne lesbaren Namen -------------------------- */
|
||
const namenlos = bedienbar.filter((e) => {
|
||
const t = (e.textContent || "").trim();
|
||
if (t.length > 0) return false;
|
||
if (e.getAttribute("aria-label")) return false;
|
||
if (e.getAttribute("aria-labelledby")) return false;
|
||
if (e.getAttribute("title")) return false;
|
||
/* `labels` GIBT ES AN JEDEM BESCHRIFTBAREN ELEMENT -- auch an
|
||
TEXTAREA (berichtigt 19.09.2026).
|
||
|
||
Vorher standen hier nur INPUT und SELECT. Ergebnis: `#sb-ueber`
|
||
wurde achtmal als „Bedienelement ohne Namen" gemeldet, obwohl
|
||
direkt darüber `<label for="sb-ueber">Ein Satz über dich</label>`
|
||
steht. Wieder ein Fehlalarm aus meiner eigenen Prüfung -- und
|
||
hätte ich ihn geglaubt, hätte ich eine Beschriftung ergänzt, die
|
||
längst da ist.
|
||
|
||
Jetzt wird `labels` gefragt, wo es das gibt, statt eine Liste von
|
||
Elementnamen zu pflegen, die irgendwann eines vergisst. */
|
||
if (e.labels && e.labels.length) return false;
|
||
return true;
|
||
}).slice(0, 4).map(name);
|
||
|
||
return {
|
||
inhalt: (document.querySelector("main")?.textContent || "").trim().length,
|
||
elemente: alle.length,
|
||
bedienbar: bedienbar.length,
|
||
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||
ueberstand, abgeschnitten, zerquetscht, klein, ueberdeckt, namenlos,
|
||
};
|
||
};
|
||
|
||
/* ---------------------------------------------------------------------
|
||
Der Rundgang
|
||
------------------------------------------------------------------- */
|
||
/* DERSELBE PFAD WIE IN DEN UEBRIGEN BROWSER-PRUEFUNGEN. Playwright
|
||
liegt nicht in diesem Repo, sondern einmal zentral daneben -- und
|
||
auf Windows braucht ein absoluter Pfad ein `file://` davor, sonst
|
||
haelt Node das "C:" fuer ein Protokoll. */
|
||
const { chromium } = await import(
|
||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||
|
||
const browser = await chromium.launch({
|
||
/* DAMIT DER SERVER SEINEN ECHTEN NAMEN SIEHT. Ohne diese Zeile
|
||
misst die Prüfung gegen 127.0.0.1 -- ein anderes Haus, andere
|
||
Kacheln, eine Adressregel, die alles durchlässt.
|
||
|
||
`--ignore-certificate-errors` gehört dazu: Der Name steht in der
|
||
HSTS-Liste, und dort lässt Chromium sonst kein eigenes Zertifikat
|
||
durch -- auch nicht mit ignoreHTTPSErrors am Kontext. */
|
||
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`,
|
||
"--ignore-certificate-errors"],
|
||
ignoreHTTPSErrors: true,
|
||
});
|
||
|
||
const ROLLEN = [
|
||
["hand", "CODE-HAND-0001", "Rechte Hand"],
|
||
["modi", "CODE-MODI-0001", "Modi"],
|
||
["gast", "CODE-GAST-0001", "Community"],
|
||
["admin", "CODE-DOGI-0001", "DogFather"],
|
||
];
|
||
const BREITEN = [[390, 844, "iPhone"], [412, 915, "Android"]];
|
||
|
||
let fehler = 0, geprueft = 0, elementeGesamt = 0, bedienbarGesamt = 0;
|
||
const probleme = [];
|
||
const melde = (t) => console.log(t);
|
||
|
||
for (const [breite, hoehe, geraet] of BREITEN) {
|
||
console.log(`\n══════════ ${geraet} · ${breite}px ══════════`);
|
||
|
||
for (const [rolle, code, wie] of ROLLEN) {
|
||
const seiten = seitenFuer(rolle);
|
||
const kontext = await browser.newContext({
|
||
viewport: { width: breite, height: hoehe },
|
||
hasTouch: true, isMobile: true, deviceScaleFactor: 3,
|
||
ignoreHTTPSErrors: true,
|
||
});
|
||
const konsole = [];
|
||
const seite = await kontext.newPage();
|
||
seite.on("console", (m) => {
|
||
if (m.type() === "error") konsole.push(m.text().slice(0, 100));
|
||
});
|
||
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message.slice(0, 100)));
|
||
/* WELCHE Datei fehlt -- die Konsole sagt es nicht (22.09.2026).
|
||
Der Rundgang meldete tagelang "404 (Not Found)" ohne Adresse.
|
||
Eine Pruefung, die einen Fehler findet, ihn aber nicht
|
||
auffindbar macht, kostet mehr Zeit als sie spart: Man weiss,
|
||
dass etwas kaputt ist, und muss es trotzdem von Hand suchen. */
|
||
seite.on("requestfailed", (r) => {
|
||
konsole.push(`Anfrage gescheitert: ${r.url().replace(BASIS, "")}`);
|
||
});
|
||
seite.on("response", (r) => {
|
||
if (r.status() === 404) konsole.push(`404: ${r.url().replace(BASIS, "")}`);
|
||
});
|
||
|
||
/* ---- Anmelden, wie ein Mensch es tut ---------------------------- */
|
||
let drin = false;
|
||
try {
|
||
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded", timeout: 20000 });
|
||
await seite.click(`.rolle[data-rolle="${rolle}"]`, { timeout: 8000 });
|
||
await seite.fill("#code", code);
|
||
/* Die Altersfrage erscheint nur für die Community. */
|
||
const frage = await seite.$("#alter-ok");
|
||
if (frage && await frage.isVisible().catch(() => false)) await frage.check();
|
||
await seite.click("button[type=submit]");
|
||
await seite.waitForURL("**/start.html", { timeout: 20000 });
|
||
drin = true;
|
||
} catch (f) {
|
||
/* EINE GESCHEITERTE ANMELDUNG IST DER SCHLIMMSTE BEFUND -- danach
|
||
wird gar nichts mehr gemessen. Sie gehoert deshalb SOFORT auf
|
||
den Bildschirm und nicht nur in die Liste am Ende. */
|
||
const wo = seite.url();
|
||
const sichtbar = await seite.evaluate(() => ({
|
||
fehler: document.getElementById("fehler")?.textContent?.trim() || "",
|
||
rollen: [...document.querySelectorAll(".rolle")].map((r) => r.dataset.rolle).join(","),
|
||
})).catch(() => ({ fehler: "(nicht lesbar)", rollen: "?" }));
|
||
melde(` FEHL ${wie.padEnd(12)} ANMELDUNG SCHEITERT`);
|
||
melde(` · ${String(f.message).slice(0, 110)}`);
|
||
melde(` · steht auf: ${wo}`);
|
||
melde(` · Meldung der Seite: "${sichtbar.fehler}"`);
|
||
melde(` · Rollen auf der Wand: ${sichtbar.rollen}`);
|
||
probleme.push(`[${geraet} ${wie}] ANMELDUNG SCHEITERT: ${String(f.message).slice(0, 80)}`);
|
||
fehler++;
|
||
}
|
||
if (!drin) { await kontext.close(); continue; }
|
||
|
||
let schlecht = 0;
|
||
const details = [];
|
||
|
||
for (const pfad of seiten) {
|
||
geprueft++;
|
||
let antwort = null;
|
||
try {
|
||
antwort = await seite.goto(BASIS + pfad,
|
||
{ waitUntil: "domcontentloaded", timeout: 20000 });
|
||
/* Kurz warten: Diese Seiten holen ihren Inhalt nach. Ohne das
|
||
misst man ein leeres Gerüst und meldet es als tadellos. */
|
||
await seite.waitForTimeout(900);
|
||
} catch (f) {
|
||
details.push(`${pfad}: lädt nicht (${String(f.message).slice(0, 50)})`);
|
||
schlecht++; continue;
|
||
}
|
||
|
||
const lage = antwort?.status() ?? 0;
|
||
const wo = new URL(seite.url()).pathname;
|
||
if (lage >= 400) {
|
||
details.push(`${pfad}: Statuscode ${lage}`); schlecht++; continue;
|
||
}
|
||
/* Eine Umleitung auf die Startseite heißt: Die Seite gibt es für
|
||
diese Rolle auf dieser Adresse nicht -- obwohl die Rechtetafel
|
||
sie erlaubt. Das ist eine Sackgasse, kein Erfolg. */
|
||
if (wo !== pfad && wo.endsWith("/start.html") && !pfad.endsWith("/start.html")) {
|
||
details.push(`${pfad}: wird auf start.html umgeleitet (Sackgasse)`);
|
||
schlecht++; continue;
|
||
}
|
||
|
||
const m = await seite.evaluate(messen);
|
||
elementeGesamt += m.elemente; bedienbarGesamt += m.bedienbar;
|
||
|
||
const kurz = pfad.replace("/workspace/", "");
|
||
if (m.inhalt < 40) { details.push(`${kurz}: fast kein Inhalt (${m.inhalt} Zeichen)`); schlecht++; }
|
||
/* GEMESSEN WIRD AM ELEMENT, NICHT AM DOKUMENT (berichtigt 19.09.2026).
|
||
|
||
Vorher entschied `scrollWidth - clientWidth`. Die Gegenprobe hat
|
||
gezeigt, dass das nichts findet: `body { overflow-x: hidden }`
|
||
schneidet den Überhang weg, also sind beide Werte immer gleich.
|
||
Die Prüfung war blind und hätte trotzdem grün gemeldet -- die
|
||
schlimmste Sorte, weil man ihr glaubt.
|
||
|
||
Jetzt zählt, ob ein sichtbares Element über den rechten Rand
|
||
ragt. Das sieht man auch dann, wenn der Balken versteckt ist. */
|
||
if (m.ueberstand.length) {
|
||
details.push(`${kurz}: ragt über den Rand — ${m.ueberstand.join(", ")}`);
|
||
schlecht++;
|
||
}
|
||
if (m.zerquetscht?.length) {
|
||
details.push(`${kurz}: Text auf fast nichts gedrückt — ${m.zerquetscht.join(", ")}`); schlecht++;
|
||
}
|
||
if (m.abgeschnitten.length) {
|
||
details.push(`${kurz}: Text abgeschnitten — ${m.abgeschnitten.join(", ")}`); schlecht++;
|
||
}
|
||
if (m.klein.length) {
|
||
details.push(`${kurz}: zu kleine Tippziele — ${m.klein.join(", ")}`); schlecht++;
|
||
}
|
||
if (m.ueberdeckt.length) {
|
||
details.push(`${kurz}: LIEGT ÜBEREINANDER — ${m.ueberdeckt.join(", ")}`); schlecht++;
|
||
}
|
||
if (m.namenlos.length) {
|
||
details.push(`${kurz}: Bedienelement ohne Namen — ${m.namenlos.join(", ")}`); schlecht++;
|
||
}
|
||
}
|
||
|
||
if (konsole.length) {
|
||
details.push(`Konsole: ${[...new Set(konsole)].slice(0, 3).join(" | ")}`);
|
||
schlecht++;
|
||
}
|
||
|
||
const zeichen = schlecht === 0 ? "ok " : "FEHL";
|
||
melde(` ${zeichen} ${wie.padEnd(12)} ${String(seiten.length).padStart(2)} Seiten`
|
||
+ (schlecht ? ` — ${schlecht} Befund${schlecht === 1 ? "" : "e"}` : " — sauber"));
|
||
/* ALLE BEFUNDE, NICHT DIE ERSTEN ZWOELF. Eine Liste, die abschneidet,
|
||
versteckt genau die Fundstellen, die man noch nicht kennt -- und
|
||
man repariert dann dreimal dasselbe, weil das Neue nie
|
||
auftaucht. */
|
||
for (const t of details) melde(` · ${t}`);
|
||
if (schlecht) { fehler += schlecht; probleme.push(...details.map((t) => `[${geraet} ${wie}] ${t}`)); }
|
||
|
||
await kontext.close();
|
||
}
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE GEGENPROBE
|
||
|
||
Eine Prüfung, die immer „alles in Ordnung" sagt, sagt nichts. Hier
|
||
werden ein zu breites Element und ein zu kleiner Knopf eingebaut --
|
||
beide MÜSSEN gefunden werden.
|
||
===================================================================== */
|
||
melde("\n --- Gegenprobe (beide Fehler MÜSSEN gefunden werden) ---");
|
||
{
|
||
/* MIT FINGER (29.09.2026). Der zweite Kontext dieser Datei hatte
|
||
keinen -- der erste schon. Ohne `hasTouch` meldet der Browser
|
||
`pointer: fine`, und keine Regel aus `@media (pointer: coarse)`
|
||
greift; dort stehen die 44-Pixel-Beruehrziele. */
|
||
const kontext = await browser.newContext({ viewport: { width: 390, height: 844 },
|
||
hasTouch: true, isMobile: true, ignoreHTTPSErrors: true });
|
||
const seite = await kontext.newPage();
|
||
await seite.goto(BASIS + "/workspace/", { waitUntil: "load" });
|
||
await seite.waitForTimeout(600);
|
||
await seite.evaluate(() => {
|
||
/* MIT TEXT, NICHT LEER (berichtigt 19.09.2026).
|
||
|
||
Der erste Entwurf hängte ein leeres <div> an -- und seit die
|
||
Messung Zierrat ausnimmt (nur Text oder Bedienbares zählt), fiel
|
||
genau dieses <div> durch den Filter. Die Gegenprobe meldete
|
||
„nichts" und bewies damit ihren eigenen Zweck: Sie hat gemerkt,
|
||
dass die Messung nichts mehr findet.
|
||
|
||
Jetzt trägt das Element Text -- also genau das, dessen
|
||
Abschneiden ein echter Fehler wäre. */
|
||
const breit = document.createElement("div");
|
||
breit.textContent = "Absichtlich zu breit, damit die Pruefung es finden muss";
|
||
breit.style.cssText = "width:900px;height:24px;background:#333;white-space:nowrap";
|
||
document.body.append(breit);
|
||
const winzig = document.createElement("button");
|
||
winzig.textContent = "x";
|
||
winzig.style.cssText = "width:18px;height:18px";
|
||
document.body.append(winzig);
|
||
|
||
/* ---- UND DIE ZWEI AUSNAHMEN VOM 22.09.2026 -------------------
|
||
Beide Regeln machen die Messung ENGER. Eine engere Messung kann
|
||
auch zu eng sein -- dann fiele echter Schaden durch, und der
|
||
Lauf bliebe gruen. Deshalb hier vier Elemente statt zwei: zwei,
|
||
die gemeldet werden MUESSEN, und zwei, die es NICHT duerfen. */
|
||
|
||
/* (a) In einem rollenden Kasten -- darf NICHT gemeldet werden.
|
||
So steht die Rechtetafel da, und so gehoert eine breite
|
||
Tabelle auf einen schmalen Schirm. */
|
||
const roller = document.createElement("div");
|
||
roller.id = "gp-roller";
|
||
roller.style.cssText = "overflow-x:auto;width:200px";
|
||
const drin = document.createElement("div");
|
||
drin.textContent = "Steht in einem Kasten, der absichtlich rollt";
|
||
drin.style.cssText = "width:900px;height:20px;white-space:nowrap";
|
||
roller.append(drin);
|
||
document.body.append(roller);
|
||
|
||
/* (b) Winzig, aber nimmt keine Klicks an -- darf NICHT gemeldet
|
||
werden. So sind die Terminpunkte im Kalender gebaut. */
|
||
const stumm = document.createElement("button");
|
||
stumm.id = "gp-stumm";
|
||
stumm.textContent = "o";
|
||
stumm.style.cssText = "width:8px;height:8px;pointer-events:none";
|
||
document.body.append(stumm);
|
||
|
||
/* ---- UND DIE AUSNAHME VOM 29.09.2026 -------------------------
|
||
(c) Zerquetschter Text IN einem abgeklemmten Kasten -- darf
|
||
NICHT gemeldet werden. So steht der Markentext in der
|
||
Kopfleiste auf schmalen Schirmen: 1x1 Pixel mit
|
||
`clip-path: inset(50%)`, damit er fuer Vorleseprogramme
|
||
bleibt und fuer das Auge verschwindet.
|
||
(d) DERSELBE TEXT OHNE ABGEKLEMMTEN KASTEN -- muss gemeldet
|
||
werden. Ohne dieses Paar waere die neue Ausnahme nicht
|
||
geprueft, sondern nur behauptet: Eine Regel, die enger
|
||
misst, kann zu eng sein, und dann faellt echter Schaden
|
||
durch. */
|
||
const huelle = document.createElement("div");
|
||
huelle.style.cssText = "position:absolute;width:1px;height:1px;"
|
||
+ "overflow:hidden;clip-path:inset(50%);white-space:nowrap";
|
||
const leise = document.createElement("span");
|
||
leise.id = "gp-leise";
|
||
leise.textContent = "Nur fuer Vorleseprogramme, nicht fuers Auge";
|
||
huelle.append(leise);
|
||
document.body.append(huelle);
|
||
|
||
const eng = document.createElement("div");
|
||
eng.style.cssText = "width:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
|
||
const laut = document.createElement("span");
|
||
laut.id = "gp-laut";
|
||
laut.textContent = "Dieser Name ist wirklich zerquetscht worden";
|
||
laut.style.cssText = "display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
|
||
eng.append(laut);
|
||
document.body.append(eng);
|
||
});
|
||
const g = await seite.evaluate(messen);
|
||
const ok1 = g.ueberstand.length > 0;
|
||
const ok2 = g.klein.length > 0;
|
||
/* Die zwei Ausnahmen duerfen NICHT in der Liste stehen. Gemessen
|
||
wird das an ihrer Groesse: 900 px waere der Roller-Inhalt, 8x8
|
||
der stumme Knopf. */
|
||
const ok3 = !g.ueberstand.some((x) => /9\d\dpx/.test(x));
|
||
const ok4 = !g.klein.some((x) => /\b8×8\b/.test(x));
|
||
/* Das Paar vom 29.09.: der abgeklemmte Text darf NICHT auftauchen,
|
||
der daneben MUSS. Nur beides zusammen beweist, dass die neue
|
||
Ausnahme genau greift und nicht weiter. */
|
||
const ok5 = g.zerquetscht.some((x) => /gp-laut/.test(x));
|
||
const ok6 = !g.zerquetscht.some((x) => /gp-leise/.test(x));
|
||
melde(` ${ok1 ? "ok " : "FEHL"} ein zu breites Element wird gemeldet (${g.ueberstand.join(", ") || "nichts"})`);
|
||
melde(` ${ok2 ? "ok " : "FEHL"} ein zu kleiner Knopf wird gemeldet (${g.klein.join(", ") || "nichts"})`);
|
||
melde(` ${ok3 ? "ok " : "FEHL"} was in einem rollenden Kasten steht, NICHT`);
|
||
melde(` ${ok4 ? "ok " : "FEHL"} was keine Klicks annimmt, auch NICHT`);
|
||
melde(` ${ok5 ? "ok " : "FEHL"} zerquetschter Text wird gemeldet (${g.zerquetscht.join(", ") || "nichts"})`);
|
||
melde(` ${ok6 ? "ok " : "FEHL"} was nur fuer Vorleseprogramme dasteht, NICHT`);
|
||
if (!ok1 || !ok2 || !ok5) { fehler += 1; probleme.push("GEGENPROBE VERSAGT — die Messung findet nichts mehr"); }
|
||
if (!ok3 || !ok4 || !ok6) { fehler += 1; probleme.push("GEGENPROBE VERSAGT — die Messung meldet Absicht als Fehler"); }
|
||
await kontext.close();
|
||
}
|
||
|
||
await browser.close();
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
|
||
melde("");
|
||
melde(` ${geprueft} Seitenaufrufe, ${elementeGesamt} Elemente, ${bedienbarGesamt} Bedienelemente gemessen`);
|
||
melde(` ${fehler} Befund${fehler === 1 ? "" : "e"}`);
|
||
melde(fehler ? " NICHT IN ORDNUNG" : " ALLES IN ORDNUNG");
|
||
process.exit(fehler ? 1 : 0);
|