pruef-handy-teamdogi (8 rot -> 17 gruen) und pruef-handy (schon gruen,
192 statt 186 Pruefungen).
ECHT WAR: Auf anleitung.html und willkommen.html waren fuenf Tippziele
40 statt 44 px hoch -- Filter, "Hingehen", "Nochmal ansehen", Pflege-
und Fehlend-Knoepfe. Daneben stand der Satz "40 Pixel hoch: Die
Hausgroesse fuer etwas, das ein Daumen trifft". Nachgezaehlt: 98 Stellen
im Haus nehmen 44, 25 nehmen 40. Ein Kommentar, der eine Zahl zur Regel
erklaert, macht sie nicht dazu.
FEHLALARM WAR: "LIEGT UEBEREINANDER -- a.an-karte__weg ⨯ a.an-karte__weg",
vier Rollen lang, auf beiden Seiten. Die Messung stimmte (93x18 px),
sichtbar war davon nichts.
Der Weg dorthin hat gedauert, und das lag an der Meldung: "93x18px"
sagt, DASS sich zwei Rechtecke schneiden, und schickt einen suchen.
Also sagt sie jetzt auch, WO beide liegen und aus welchen Vorfahren sie
kommen -- und damit war es in einem Lauf klar:
a.an-karte__weg in an-karte@4510+375 < an-stufe__gitter@4165+2256
< an-stufe__falte@4065+55
Der Abschnitt ist 55 px hoch und traegt `overflow: hidden`; sein Gitter
faengt 45 px UNTER dessen Unterkante an. Der Browser schneidet alles
davon weg.
`sichtbar()` fragte bis heute nur das Element selbst: Groesse,
visibility, display, Deckkraft. Alle vier koennen tadellos sein und das
Ding trotzdem unerreichbar. Jetzt kommt `imAusschnitt()` dazu:
Schneidet ein Vorfahr mit `overflow: hidden` es vollstaendig weg, zaehlt
es nicht -- fuer alle drei Messungen dieser Datei (zu klein, Ueberstand,
Ueberdeckung).
ZWEI VERSUCHE DAVOR WAREN FALSCH, beide an mir: Erst fragte ich nach
`details:not([open])` -- die Abschnitte tragen `[open]`, also griff es
nicht. Dann setzte ich die Rechnung in die falsche Filterkette (die fuer
zu kleine Ziele statt die fuer `bedienbar`). Geprueft wird jetzt die
WIRKUNG und nicht die Bauform: Rechtecke veralten nicht mit der
Bauweise.
Die eingebauten Gegenproben der Datei laufen weiter an (ein zu breites
Element und ein zu kleiner Knopf werden gemeldet) -- der Filter blendet
also nichts Echtes aus.
Co-Authored-By: Claude Opus 5 <[email protected]>
1051 lines
50 KiB
JavaScript
1051 lines
50 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 = () => {
|
||
/* ==== WAS AUS SEINEM AUSSCHNITT HERAUSRAGT, IST NICHT DA ============
|
||
(07.10.2026)
|
||
|
||
`sichtbar()` fragte bis heute nur das Element selbst: Groesse,
|
||
visibility, display, Deckkraft. Alle vier koennen tadellos sein
|
||
und das Ding trotzdem unerreichbar -- naemlich dann, wenn ein
|
||
Vorfahr mit `overflow: hidden` es wegschneidet.
|
||
|
||
GEFUNDEN AN EINEM FEHLALARM, der vier Rollen lang rot war:
|
||
„anleitung.html: LIEGT UEBEREINANDER -- a.an-karte__weg ⨯
|
||
a.an-karte__weg". Die Messung stimmte (93x18 px Ueberdeckung),
|
||
nur sah das niemand. Nachgemessen steht beides in
|
||
`.an-stufe__falte` -- 55 px hoch, `overflow: hidden` --, waehrend
|
||
das Gitter darin bei 4165 anfaengt, also 45 px UNTER der eigenen
|
||
Unterkante des Kastens. Der Browser schneidet alles davon weg.
|
||
|
||
GEPRUEFT WIRD DIE WIRKUNG, NICHT DIE BAUFORM. Ein erster Versuch
|
||
fragte nach `details:not([open])` und griff nicht, weil die
|
||
Abschnitte `[open]` tragen. Rechtecke veralten nicht mit der
|
||
Bauweise: Schneidet ein Vorfahr das Element vollstaendig weg,
|
||
zaehlt es nicht -- heute beim <details>, morgen anderswo.
|
||
|
||
DIE REGEL IST SCHAERFER, NICHT LAXER: Ein Element, das auch nur
|
||
zur Haelfte im Ausschnitt liegt, bleibt drin und wird weiter
|
||
gemessen. Und klappt jemand den Abschnitt auf, gilt wieder alles. */
|
||
const imAusschnitt = (e) => {
|
||
const r = e.getBoundingClientRect();
|
||
for (let n = e.parentElement; n && n !== document.body; n = n.parentElement) {
|
||
const s = getComputedStyle(n);
|
||
if (s.overflowX === "visible" && s.overflowY === "visible") continue;
|
||
const rn = n.getBoundingClientRect();
|
||
if (rn.width === 0 || rn.height === 0) continue;
|
||
if (Math.min(r.right, rn.right) - Math.max(r.left, rn.left) <= 0) return false;
|
||
if (Math.min(r.bottom, rn.bottom) - Math.max(r.top, rn.top) <= 0) return false;
|
||
}
|
||
return true;
|
||
};
|
||
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
|
||
&& imAusschnitt(e);
|
||
};
|
||
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;
|
||
/* ==== WAS IN EINEM ZUGEKLAPPTEN <details> STECKT, ZAEHLT NICHT ====
|
||
(07.10.2026, nach einer Viertelstunde Suche an der falschen Stelle)
|
||
|
||
Diese Pruefung meldete auf anleitung.html und willkommen.html
|
||
vier Rollen lang „LIEGT UEBEREINANDER -- a.an-karte__weg ⨯
|
||
a.an-karte__weg". Die Messung stimmte: zwei Verweise, 93 px
|
||
breit, 18 px Ueberdeckung. Nur konnte sie niemand sehen.
|
||
|
||
Nachgemessen stehen beide in verschiedenen `.an-stufe__falte` --
|
||
zwei <details>, je 55 px hoch, mit `overflow: hidden`. Ihr Inhalt
|
||
ist 2256 bzw. 5754 px hoch und wird vollstaendig abgeschnitten.
|
||
|
||
DER GRUND IST NEUER ALS DIESE PRUEFUNG: Chromium gibt dem Inhalt
|
||
eines GESCHLOSSENEN <details> seit kurzem weiterhin ein Layout
|
||
(`content-visibility: hidden` statt `display: none`), damit sich
|
||
das Auf- und Zuklappen animieren laesst. Rechtecke gibt es also,
|
||
sichtbar oder antippbar ist davon nichts.
|
||
|
||
Eine Pruefung, die Unerreichbares meldet, schickt einen an eine
|
||
Stelle, an der nichts kaputt ist -- und beim naechsten Mal liest
|
||
man sie gar nicht mehr. Deshalb hier heraus. Die Regel bleibt
|
||
scharf: Ein zugeklappter Abschnitt wird aufgeklappt, sobald
|
||
jemand darauf tippt, und DANN gilt sie wieder. */
|
||
if (e.closest("details:not([open])")) 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;
|
||
/* WO liegen die beiden? „93×18px Ueberdeckung" sagt, DASS etwas
|
||
uebereinanderliegt, und schickt einen danach suchen. Die Lage
|
||
liegt hier ohnehin vor -- sie wurde nur weggeworfen. Mit ihr
|
||
sieht man auf einen Blick, ob zwei Nachbarn sich beruehren
|
||
oder ob eines irgendwo ganz anders klebt. (07.10.2026) */
|
||
const lage = (r) => `${Math.round(r.left)},${Math.round(r.top)} ${Math.round(r.width)}×${Math.round(r.height)}`;
|
||
/* UND WOHER KOMMEN SIE? Zwei gleich benannte Elemente sagen nichts
|
||
darueber, ob sie Nachbarn sind oder aus ganz verschiedenen
|
||
Ecken stammen. Der naechste Vorfahr mit einer Klasse beantwortet
|
||
genau das -- und erspart das Raten, das mich hier eine
|
||
Viertelstunde gekostet hat. */
|
||
const woher = (e) => {
|
||
const teile = [];
|
||
for (let n = e.parentElement, i = 0; n && i < 3; n = n.parentElement, i++) {
|
||
if (!n.className || typeof n.className !== "string") continue;
|
||
const r = n.getBoundingClientRect();
|
||
teile.push(`${n.className.split(" ")[0]}@${Math.round(r.top)}+${Math.round(r.height)}`
|
||
+ `/${getComputedStyle(n).position}`);
|
||
}
|
||
return teile.join(" < ") || "?";
|
||
};
|
||
ueberdeckt.push(`${name(a)} in ${woher(a)} [${lage(ra)}] ⨯ ${name(b)} in ${woher(b)} [${lage(rb)}]`
|
||
+ ` = ${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"]];
|
||
|
||
/** Meldet sich auf der Team-Adresse an und wartet, bis die Startseite steht.
|
||
*
|
||
* EINE STELLE, ZWEI AUFRUFER (03.10.2026). Diese sechs Zeilen standen
|
||
* in der Schleife unten, und als ich sie fuer eine zweite Messung
|
||
* gebraucht habe, habe ich sie abgeschrieben -- und dabei eine
|
||
* aeltere Fassung erwischt: ohne `isVisible()` und mit `click()`
|
||
* statt `check()`. Ergebnis war ein Zeitablauf nach 30 Sekunden an
|
||
* der Altersfrage, die es fuer DogFather gar nicht gibt. Eine halbe
|
||
* Stunde fuer eine Abschrift.
|
||
*
|
||
* Die Altersfrage erscheint nur fuer die Community -- deshalb erst
|
||
* fragen, ob sie ueberhaupt sichtbar ist. */
|
||
async function anmeldenAufCrew(seite, rolle, code) {
|
||
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded", timeout: 20000 });
|
||
await seite.click(`.rolle[data-rolle="${rolle}"]`, { timeout: 8000 });
|
||
await seite.fill("#code", code);
|
||
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 });
|
||
}
|
||
|
||
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 anmeldenAufCrew(seite, rolle, code);
|
||
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();
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE "FUER WEN"-REIHE GIBT ES AUF crew. NICHT MEHR (03.10.2026)
|
||
|
||
Filipe, mit Bildschirmfoto der Leiste "FUER WEN · Alle 3 2 ·
|
||
Marina 1 · Diene 0": "brauchen wir nicht auf der aufgaben seite."
|
||
|
||
WARUM DAS HIER GEPRUEFT WIRD UND NICHT IM AUFGABENBRETT: Es ist
|
||
DIESELBE Reihe in beiden Haeusern, und sie bleibt bei Spicy Media
|
||
-- dort filtern Scouts und Manager durch ihre Creator. Die
|
||
Gegenprobe steht deshalb schon in pruef-aufgabenbrett ("Mara sieht
|
||
Alle, Tili, Luna"), und nur die crew-Seite fehlte. Ohne diese
|
||
Zeilen waere die Trennung eine Behauptung: Niemand wuerde merken,
|
||
wenn die Reihe auf crew. zurueckkaeme -- oder wenn sie der Agentur
|
||
nebenbei mit abhanden kaeme.
|
||
|
||
DIE GEGENPROBE STEHT DANEBEN, und sie ist hier das Wichtigste:
|
||
"Reihe nicht da" waere auch auf einer leeren oder kaputten Seite
|
||
wahr. Deshalb muss die Schwester-Reihe (Alle / Heute faellig /
|
||
Ueberfaellig) im selben Atemzug SICHTBAR sein. Erst beides zusammen
|
||
heisst "die Seite steht, und genau dieses eine Stueck fehlt".
|
||
===================================================================== */
|
||
{
|
||
const kontext = await browser.newContext({
|
||
viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true,
|
||
ignoreHTTPSErrors: true,
|
||
});
|
||
const seite = await kontext.newPage();
|
||
let gemessen = null;
|
||
try {
|
||
await anmeldenAufCrew(seite, "admin", "CODE-DOGI-0001");
|
||
await seite.goto(BASIS + "/workspace/aufgaben.html",
|
||
{ waitUntil: "domcontentloaded", timeout: 20000 });
|
||
await seite.waitForTimeout(1200);
|
||
gemessen = await seite.evaluate(() => {
|
||
const r = document.getElementById("creator-reihe");
|
||
const a = document.getElementById("ausschnitte");
|
||
return {
|
||
reiheDa: !!r && r.hidden === false,
|
||
knoepfe: document.querySelectorAll(".creator-knopf").length,
|
||
schwesterDa: !!a && a.hidden === false,
|
||
schwesterKnoepfe: a ? a.querySelectorAll("button").length : 0,
|
||
};
|
||
});
|
||
} catch (f) {
|
||
melde(` FEHL "Fuer wen"-Reihe: nicht messbar -- ${String(f.message).slice(0, 160)}`);
|
||
probleme.push(`"Fuer wen"-Reihe: nicht messbar (${String(f.message).slice(0, 70)})`);
|
||
fehler++;
|
||
}
|
||
if (gemessen) {
|
||
geprueft += 3;
|
||
const g1 = gemessen.schwesterDa && gemessen.schwesterKnoepfe >= 2;
|
||
const g2 = !gemessen.reiheDa;
|
||
const g3 = gemessen.knoepfe === 0;
|
||
melde("");
|
||
melde(` ${g1 ? "ok " : "FEHL"} die Aufgabenseite steht wirklich `
|
||
+ `(${gemessen.schwesterKnoepfe} Knoepfe in der Ausschnitt-Reihe)`);
|
||
melde(` ${g2 ? "ok " : "FEHL"} und die "Fuer wen"-Reihe ist weg (sichtbar: ${gemessen.reiheDa})`);
|
||
melde(` ${g3 ? "ok " : "FEHL"} auch kein einzelner Knopf davon (${gemessen.knoepfe})`);
|
||
if (!g1) { fehler++; probleme.push('"Fuer wen": die Seite stand gar nicht -- die Messung beweist nichts'); }
|
||
if (!g2 || !g3) { fehler++; probleme.push('"Fuer wen"-Reihe steht auf crew. wieder da'); }
|
||
}
|
||
await kontext.close();
|
||
}
|
||
|
||
await browser.close();
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
|
||
/* DIE BEFUNDE GEHOEREN UNTER DIE ZAHL (03.10.2026).
|
||
|
||
`probleme` wurde seit jeher gefuellt und NIE ausgegeben: Am Ende
|
||
stand "1 Befund" und sonst nichts. Wer das liest, weiss, DASS etwas
|
||
ist, aber nicht was -- und muss den ganzen Lauf noch einmal
|
||
anstossen, um es zu erfahren. Genau das ist mir heute passiert.
|
||
Ein Befund ohne Grund ist eine Fundstelle, die man ueberliest. */
|
||
if (probleme.length) {
|
||
melde("");
|
||
melde(" --- Befunde im Einzelnen ---");
|
||
for (const t of probleme) melde(` · ${t}`);
|
||
}
|
||
|
||
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);
|