Gestern Mittag gemessen: 39 `newContext`-Aufrufe nehmen ihre Breite
aus einer Variablen und setzen kein `hasTouch`. Ohne das meldet der
Browser `pointer: fine`, und KEINE Regel aus `@media (pointer:
coarse)` greift — dort stehen im ganzen Haus die 44-Pixel-
Beruehrziele, die ausgeblendeten Tastenkuerzel und die
eingeklappte Reiterleiste.
Was das anrichtet, war an pruef-breiten zu sehen: drei Befunde auf
320, 390 und 430 Pixeln, die mit dem Finger allesamt verschwanden —
Messfehler, keine Fehler.
37 DAVON HABEN IHN JETZT. Nur `hasTouch`, nicht `isMobile`: Gefragt
ist genau die eine Sache, um die es geht. `isMobile` waere eine
zweite Aenderung in derselben Zeile, und wenn danach etwas anders
aussieht, wuesste niemand, welche von beiden es war.
ZWEI BLEIBEN STEHEN, beide in pruef-grosscheck.mjs. Die Aenderung
dort waere ein Zweizeiler; sie zu pruefen hiesse, 206 Seiten ueber
vier Rollen laufen zu lassen, und das braucht Filipes Zusage. Eine
Aenderung, die ich nicht pruefen darf, liefere ich nicht aus.
Grundlinie in pruef-fingermass steht deshalb auf 2.
JEDE EINZELN NACHGELAUFEN — 37 Laeufe:
34 gruen, darunter pruef-handy 186, pruef-material 159,
pruef-start-ansicht 160, pruef-kalender 141, pruef-erwaehnung 129,
pruef-bewerbung-aufgaben 163, pruef-neue-seiten 109
3 mit Befunden, ALLE DREI VORBESTEHEND (Gegenprobe: alter Stand
derselben Datei, gleicher Lauf, gleiche Zahl):
pruef-browser 3 (WebKit startet auf diesem Rechner nicht)
pruef-chat-anhaenge 2
pruef-crew-wand-bild 3
EINE ZEITBOMBE GEFUNDEN UND ENTSCHAERFT
pruef-dabei-optik meldete „Haekchen: 0 -> 0" — aber nur, wenn vier
andere Pruefungen gleichzeitig liefen. Allein: „0 -> 1", gruen.
Dort stand `waitForTimeout(250)` mit der Begruendung „250 ms sind
reichlich ueber den 160" (der Dauer der Blende). Auf einem Rechner,
auf dem nebenher vier Browser messen, sind sie es nicht. Die
Pruefung war damit gruen, solange nichts anderes lief, und rot im
Gesamtlauf — also genau dann, wenn niemand sie einzeln nachstellen
kann.
Eine Wartezeit ist eine Annahme ueber den Rechner. Gewartet wird
jetzt auf das, worauf es ankommt: dass das Haekchen da ist. Laeuft
die Frist ab, faellt die Pruefung mit dem ECHTEN Wert um und nicht
mit einem Messfehler. Gegenprobe: unter derselben vierfachen Last,
die sie vorher rot gemacht hat, jetzt gruen.
UND DIE GRUNDLINIE WIEDER STRENG. Ich hatte sie kurz auf
„hoechstens" gestellt — damit haette ein Rueckgang stillschweigend
Platz fuer die naechste Suende gedeckt. Genau davor warnt der Kopf
derselben Datei bei der anderen Grundlinie, und ich habe es eine
Stunde spaeter selbst falsch gemacht.
Co-Authored-By: Claude Opus 5 <[email protected]>
774 lines
38 KiB
JavaScript
774 lines
38 KiB
JavaScript
/* ===================================================================
|
|
Die Handy-Pruefung.
|
|
|
|
Auf einem Telefon geht anderes kaputt als am Rechner, und man sieht
|
|
es beim Entwickeln nie: Ein Knopf steht 3 px aus dem Bild, eine
|
|
Ueberschrift ist auf halbem Wort abgeschnitten, ein Feld ist so
|
|
schmal, dass man es nicht trifft. Alles davon faellt erst auf, wenn
|
|
jemand das Geraet in der Hand hat.
|
|
|
|
Geprueft wird auf DREI echten Geraetegroessen, jede Seite einzeln:
|
|
|
|
1. Nichts steht ueber waagerechtes Scrollen ist auf einem Handy
|
|
der schlimmste Fehler -- die Seite ruckelt
|
|
und man verliert den Rand
|
|
2. Kein Text abgeschnitten gemessen wird die tatsaechliche
|
|
Textbreite gegen die verfuegbare, nicht
|
|
"sieht ganz aus"
|
|
3. Beruehrziele gross genug 24x24 CSS-Pixel ist das Mindestmass
|
|
(WCAG 2.2, Kriterium 2.5.8). Darunter
|
|
trifft man mit dem Daumen daneben
|
|
4. Nichts ueberlappt zwei Bedienelemente duerfen sich nicht
|
|
ueberdecken -- man traefe das falsche
|
|
5. Keine Fehler in der Konsole
|
|
=================================================================== */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-handy-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
/* DER PORT MUSS MEINER SEIN (nachgeruestet 19.09.2026).
|
|
Ohne diese Zeile startet der eigene Server bei belegtem Port
|
|
STILL nicht -- und alles Folgende misst gegen einen fremden
|
|
Stand. Das ist der dritte Ausgang: belegt ist etwas anderes
|
|
als kaputt. */
|
|
const PORT = await eigenerPort(import.meta, "pruef-handy");
|
|
process.env.PORT = `${PORT}`;
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
await import("./index.js");
|
|
/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort
|
|
zwei Auffangnetze ein, die einen Absturz nur protokollieren --
|
|
richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese
|
|
Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der
|
|
Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */
|
|
notbremse(480_000, "pruef-handy");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 400_000).unref?.();
|
|
|
|
/* ORTSZEIT, nicht UTC. Die Anwendung rechnet "heute" nach der Uhr des
|
|
Benutzers; toISOString() liefert dagegen UTC und damit zwischen
|
|
Mitternacht und 2 Uhr den VORTAG. In diesem Fenster schlug diese
|
|
Pruefung fehl, ohne dass etwas kaputt war. */
|
|
const tagText = (d = new Date()) =>
|
|
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`
|
|
+ `-${String(d.getDate()).padStart(2, "0")}`;
|
|
|
|
let fehler = 0;
|
|
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
|
|
|
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)); }
|
|
}
|
|
|
|
/* Echte Daten statt leerer Seiten: Eine leere Liste kann nicht
|
|
ueberlaufen. Geprueft wird mit LANGEN Namen und vollen Listen --
|
|
genau dort bricht es. */
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
const heute = tagText();
|
|
const gestern = tagText(new Date(Date.now() - 86400_000));
|
|
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;
|
|
}
|
|
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
const idScout = anlegen("Patrick-Maximilian", "scout", "CODE-SCOU-0001");
|
|
const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001");
|
|
/* Die beiden fehlenden Rollen -- gebraucht fuer die Mittenpruefung ganz
|
|
unten. Filipe: "das soll bei jedem und jeder rolle verbessert
|
|
werden", und eine Pruefung an drei von fuenf Rollen haette genau
|
|
diese Aussage nicht belegen koennen. */
|
|
const idSpicy = anlegen("Cigdem", "spicy", "CODE-SPIC-0001");
|
|
const idMgr = anlegen("Mara", "manager", "CODE-MANA-0001");
|
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idScout, jetzt);
|
|
|
|
const LANG = "Ein sehr langer Eintrag ohne Leerzeichenumbruch fuer die Handypruefung";
|
|
for (const [status, frist] of [["offen", gestern], ["offen", heute], ["arbeit", null],
|
|
["review", null], ["erledigt", null]]) {
|
|
d.prepare(`INSERT INTO aufgaben (titel, status, creator_id, verantwortlich_id, frist,
|
|
prioritaet, erstellt, erstellt_von) VALUES (?,?,?,?,?,'hoch',?,?)`)
|
|
.run(LANG, status, idTili, idTili, frist, jetzt, idDogi);
|
|
}
|
|
for (const [art, stunde] of [["call", "10:00"], ["termin", "14:30"], ["review", "18:00"]]) {
|
|
d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, teilnehmer_id,
|
|
erledigt, erstellt, erstellt_von) VALUES (?,?,?,60,?,?,0,?,?)`)
|
|
.run(LANG, art, heute + "T" + stunde, idTili, idTili, jetzt, idDogi);
|
|
}
|
|
d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, dringlichkeit, status,
|
|
creator_id, erstellt, erstellt_von)
|
|
VALUES ('technik','problem',?,?,'hoch','offen',?,?,?)`)
|
|
.run(LANG, heute, idTili, jetzt, idDogi);
|
|
d.close();
|
|
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
|
|
/* ---------- WELCHE SEITEN? ALLE. -----------------------------------------
|
|
|
|
Hier stand bis zum 06.09.2026 eine Liste von Hand -- und sie war
|
|
veraltet, ohne dass es jemandem auffiel: chat.html, leistung.html und
|
|
steckbrief.html fehlten darin, also DREI von neunzehn Seiten waren nie
|
|
auf einem Handy gemessen worden. Der Chat ausgerechnet ist die Seite,
|
|
die man am ehesten unterwegs benutzt.
|
|
|
|
Das ist genau die Sorte Fehler, die eine Liste macht: Sie altert in
|
|
dem Moment, in dem jemand weiterarbeitet, und sie sagt nichts dazu.
|
|
Der Lauf blieb gruen, die Zahl der Pruefungen stimmte -- gemessen
|
|
wurde trotzdem zu wenig.
|
|
|
|
Deshalb wird die Liste jetzt aus dem VERZEICHNIS gelesen. Eine neue
|
|
Seite ist damit von selbst dabei, so wie sie durch die Schranke in
|
|
workspace.js von selbst geschuetzt ist -- derselbe Gedanke: Der
|
|
sichere Rueckfall muss der VORGABEwert sein, nicht der, an den man
|
|
denken muss.
|
|
|
|
Ausgenommen ist einzig index.html -- das ist die Anmeldeseite, sie
|
|
wird weiter unten eigens geprueft (vor der Anmeldung, denn danach
|
|
leitet sie weiter). */
|
|
const { readdirSync } = await import("node:fs");
|
|
const { dirname } = await import("node:path");
|
|
const { fileURLToPath } = await import("node:url");
|
|
/* NICHT process.cwd(): Diese Datei wird mal aus dem Wurzelverzeichnis
|
|
gestartet (tools/alles-pruefen.mjs) und mal aus server/ von Hand. Im
|
|
zweiten Fall zeigte cwd auf server/, dort gibt es kein workspace/,
|
|
und readdirSync wuerfe -- oder faende, schlimmer, ein anderes
|
|
Verzeichnis. Der Ort DIESER Datei steht dagegen fest. */
|
|
const SEITEN_ORDNER = join(dirname(fileURLToPath(import.meta.url)), "..", "workspace");
|
|
const ALLE_SEITEN = readdirSync(SEITEN_ORDNER)
|
|
.filter((n) => n.endsWith(".html") && n !== "index.html")
|
|
.sort();
|
|
|
|
/* Ein paar Seiten brauchen eine Abfrage, sonst zeigen sie nur ihre
|
|
leere Huelle. Fuer die steht hier, WOMIT sie aufgerufen werden. */
|
|
const MIT_ABFRAGE = { "bereich.html": "?b=live" };
|
|
const SEITEN = ALLE_SEITEN.map((n) => n + (MIT_ABFRAGE[n] || ""));
|
|
|
|
/* Und die Zahl gehoert in die BEDINGUNG, nicht nur in den Meldetext:
|
|
Faende readdirSync nichts (falsches Arbeitsverzeichnis), liefe die
|
|
Schleife unten null Mal -- und der Lauf waere gruen, ohne eine
|
|
einzige Seite angesehen zu haben. Genau der Fehler, den ich schon
|
|
zweimal hatte. */
|
|
if (SEITEN.length < 15) {
|
|
console.log(` FEHL nur ${SEITEN.length} Seiten in ${SEITEN_ORDNER} gefunden `
|
|
+ "-- die Handypruefung kann so nichts messen");
|
|
process.exit(1);
|
|
}
|
|
console.log(`${SEITEN.length} Workspace-Seiten werden gemessen: ${SEITEN.join(", ")}`);
|
|
|
|
/* Drei echte Geraete, nicht "irgendwas Schmales":
|
|
320 iPhone SE (1. Gen) -- das schmalste noch benutzte Geraet
|
|
390 iPhone 14/15, das haeufigste Mass
|
|
412 Pixel 8 / Galaxy S24, typisches Android */
|
|
const GERAETE = [[320, 568, "sehr schmal"], [390, 844, "iPhone"], [412, 915, "Android"]];
|
|
|
|
const messen = () => ({
|
|
/* 1. Waagerechtes Scrollen. Der schlimmste Fehler auf einem Handy. */
|
|
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
schuld: [...document.querySelectorAll("body *")]
|
|
.filter((e) => {
|
|
const r = e.getBoundingClientRect();
|
|
/* Nur was wirklich sichtbar ist. Ein Wasserzeichen hinter
|
|
overflow:hidden meldet ein Rechteck, das nie zu sehen war --
|
|
das hat frueher schon einmal falschen Alarm ausgeloest. */
|
|
if (r.width < 1 || r.height < 1) return false;
|
|
const st = getComputedStyle(e);
|
|
if (st.visibility === "hidden" || st.display === "none") return false;
|
|
/* Steckt es in einem Vorfahren mit overflow:hidden? Dann ist es
|
|
abgeschnitten und steht nicht wirklich ueber. */
|
|
for (let v = e.parentElement; v; v = v.parentElement) {
|
|
const vs = getComputedStyle(v);
|
|
if (vs.overflow !== "visible" || vs.overflowX !== "visible") return false;
|
|
}
|
|
return r.right > window.innerWidth + 1;
|
|
})
|
|
.map((e) => `${e.tagName}.${String(e.className).slice(0, 26)}(+${Math.round(e.getBoundingClientRect().right - window.innerWidth)}px)`)
|
|
.slice(0, 4),
|
|
|
|
/* 2. Abgeschnittener Text. Gemessen wird die tatsaechliche Breite
|
|
gegen die verfuegbare -- nicht "sieht ganz aus". Elemente, die
|
|
ausdruecklich kuerzen duerfen (ellipsis), sind ausgenommen: Dort
|
|
ist das Kuerzen die Absicht und der volle Text steht woanders. */
|
|
abgeschnitten: [...document.querySelectorAll("h1, h2, h3, p, span, a, label, button, li, td, th")]
|
|
.filter((e) => {
|
|
if (e.children.length) return false;
|
|
const t = e.textContent.trim();
|
|
if (t.length < 3) return false;
|
|
const st = getComputedStyle(e);
|
|
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
if (st.textOverflow === "ellipsis") return false;
|
|
if (st.overflow === "hidden" && st.whiteSpace === "nowrap") return false;
|
|
/* ==== NUR WO DIE MESSUNG UEBERHAUPT ETWAS BEDEUTET (25.09.2026)
|
|
|
|
`scrollWidth` und `clientWidth` gibt es nur fuer Elemente mit
|
|
einem eigenen Kasten. Bei `display: inline` liefert Chromium
|
|
fuer `clientWidth` glatt NULL -- und damit ist jeder Text
|
|
breiter als sein Kasten.
|
|
|
|
GEMELDET WURDE dadurch auf team.html: „Mara" (18>0), „Noch
|
|
niemand zugeteilt" (14>0), „Patrick-Maximilian" (18>15). Alle
|
|
drei stehen vollstaendig da; sie sind nur inline gesetzt.
|
|
|
|
Ein Inline-Element SCHNEIDET auch nichts ab -- es bricht um.
|
|
Abgeschnitten wird es erst durch einen Vorfahren mit
|
|
`overflow: hidden`, und dann greift die Zeile darueber.
|
|
|
|
DER RICHTIGE UMGANG MIT EINER UNGUELTIGEN MESSUNG ist, sie
|
|
nicht zu machen -- nicht, ihr Ergebnis zu glauben. */
|
|
if (st.display === "inline") return false;
|
|
/* ==== UND WAS FUERS AUGE WEGGENOMMEN IST (25.09.2026) ========
|
|
|
|
Fuer Vorleseprogramme steht auf mehreren Seiten Text da, den
|
|
man nicht sieht: ein echtes `<select>` unter dem selbst
|
|
gebauten Sicht-Umschalter, Beschriftungen zu Symbolknoepfen.
|
|
Versteckt wird so etwas NICHT mit `display: none` (dann
|
|
ueberspringen manche Vorleseprogramme es), sondern mit einem
|
|
1x1-Ausschnitt -- `clip-path: inset(50%)`.
|
|
|
|
Solche Elemente sind ein Pixel gross und ihr Inhalt breiter.
|
|
`scrollWidth > clientWidth` ist damit IMMER wahr, und die
|
|
Meldung lautete: „Mara" (18>0), „Noch niemand zugeteilt"
|
|
(14>0). Nichts davon ist abgeschnitten -- es ist mit Absicht
|
|
nicht zu sehen.
|
|
|
|
Dieselbe Erkennung benutzt `pruef-neue-seiten` beim
|
|
Ueberlappen; sie hat hier nur gefehlt. */
|
|
if (st.clipPath && st.clipPath !== "none") return false;
|
|
if (st.clip && st.clip !== "auto") return false;
|
|
return e.scrollWidth > e.clientWidth + 2;
|
|
})
|
|
.map((e) => `"${e.textContent.trim().slice(0, 22)}" (${e.scrollWidth}>${e.clientWidth}`
|
|
+ `, ${e.tagName.toLowerCase()}.${String(e.className).split(" ")[0]}`
|
|
+ `, ${getComputedStyle(e).display}, w=${Math.round(e.getBoundingClientRect().width)})`)
|
|
.slice(0, 5),
|
|
|
|
/* 3. Beruehrziele. 24x24 CSS-Pixel ist das Mindestmass nach WCAG 2.2
|
|
(2.5.8). Gemessen wird das Ziel selbst ODER sein umschliessender
|
|
Bereich -- ein 16 px grosses Symbol in einem 44 px grossen Knopf
|
|
ist in Ordnung.
|
|
|
|
LINKS IN EINEM SATZ SIND AUSGENOMMEN -- und zwar nicht, weil es
|
|
bequemer ist, sondern weil 2.5.8 das selbst so sagt: "Inline: The
|
|
target is in a sentence or its size is otherwise constrained by
|
|
the line-height of non-target text."
|
|
|
|
Die Pruefung zitierte bis zum 14.09.2026 die Regel, aber nicht
|
|
ihre Ausnahme. Gemeldet wurden dadurch die zwei Verweise im
|
|
Fliesstext der Treff-Regeln ("Das Rudel", "Regeln & Hilfe", je
|
|
20 px hoch). Sie auf 24 px zu bringen hiesse, die Zeilenhoehe des
|
|
Absatzes zu sprengen -- man wuerde also einen Text kaputtmachen,
|
|
um eine Regel zu erfuellen, die diesen Text ausdruecklich nicht
|
|
meint.
|
|
|
|
ENG GEFASST: Nur <a>, nur wirklich inline dargestellt, und nur
|
|
wenn im selben Absatz auch Text steht, der NICHT zum Link gehoert.
|
|
Ein allein stehender Link bleibt ein Beruehrziel. */
|
|
klein: (() => {
|
|
const imSatz = (e) => {
|
|
if (e.tagName !== "A") return false;
|
|
if (!getComputedStyle(e).display.startsWith("inline")) return false;
|
|
const eltern = e.parentElement;
|
|
if (!eltern) return false;
|
|
const daneben = [...eltern.childNodes]
|
|
.filter((n) => n !== e)
|
|
.map((n) => (n.textContent || "").trim())
|
|
.join(" ").trim();
|
|
return daneben.length >= 10; // das ist der Satz drumherum
|
|
};
|
|
return [...document.querySelectorAll("button, a[href], input, select, textarea, [role=button]")]
|
|
.filter((e) => {
|
|
const r = e.getBoundingClientRect();
|
|
if (r.width < 1 || r.height < 1) return false;
|
|
const st = getComputedStyle(e);
|
|
if (st.display === "none" || st.visibility === "hidden") return false;
|
|
/* ==== WAS KEINE BERUEHRUNG ANNIMMT (25.09.2026) ==============
|
|
|
|
`pointer-events: none` heisst: Dieses Element ist fuer den
|
|
Finger nicht da -- die Beruehrung geht durch es hindurch an
|
|
das, was darunter liegt. Es als zu kleines Beruehrziel zu
|
|
melden, ist so, als beanstande man die Groesse eines
|
|
gemalten Knopfs.
|
|
|
|
GEMELDET WURDEN die Terminpunkte im Monatsraster des
|
|
Kalenders: 8 x 8 px, `font-size: 0`, `pointer-events: none`.
|
|
Sie sind mit Absicht so -- drei Termine an einem Tag sollen
|
|
drei Punkte sein und nicht drei Zeilen, und angetippt wird
|
|
die ZELLE, die den Tagesdialog oeffnet. Die Begruendung steht
|
|
ausfuehrlich in kalender.css.
|
|
|
|
Dieselbe Ausnahme kennt `pruef-breiten` seit jeher; sie hat
|
|
hier nur gefehlt. */
|
|
if (st.pointerEvents === "none") return false;
|
|
if (imSatz(e)) return false;
|
|
return r.width < 24 || r.height < 24;
|
|
})
|
|
.map((e) => {
|
|
const r = e.getBoundingClientRect();
|
|
return `${e.tagName}"${(e.textContent || e.getAttribute("aria-label") || "").trim().slice(0, 14)}" `
|
|
+ `${Math.round(r.width)}x${Math.round(r.height)}`;
|
|
})
|
|
.slice(0, 5);
|
|
})(),
|
|
|
|
/* 4. Ueberlappende Bedienelemente. Zwei Knoepfe, die sich decken,
|
|
heissen: Man trifft den falschen. Geprueft wird der Mittelpunkt --
|
|
liegt dort ein ANDERES Bedienelement, ist das Ziel verdeckt. */
|
|
verdeckt: [...document.querySelectorAll("button, a[href], input, select")]
|
|
.filter((e) => {
|
|
const r = e.getBoundingClientRect();
|
|
if (r.width < 8 || r.height < 8) return false;
|
|
if (r.top < 0 || r.bottom > window.innerHeight) return false;
|
|
const st = getComputedStyle(e);
|
|
if (st.display === "none" || st.visibility === "hidden" || st.pointerEvents === "none") return false;
|
|
/* NICHT ZAEHLEN, WAS IN EINEM ZUGEKLAPPTEN <details> STECKT.
|
|
Chromium versteckt dessen Inhalt ueber `content-visibility`,
|
|
nicht ueber `display` -- die Kaesten behalten deshalb eine
|
|
Groesse und sehen fuer jede Messung aus wie sichtbare
|
|
Bedienelemente. Am 06.09.2026 meldete das einen "verdeckten"
|
|
Knopf, den auf dem Bildschirm niemand sehen konnte. Eine
|
|
Warnung ueber etwas Unsichtbares gewoehnt man sich ab zu
|
|
lesen. */
|
|
if (e.closest("details:not([open])")) return false;
|
|
const oben = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
|
|
return oben && oben !== e && !e.contains(oben) && !oben.contains(e);
|
|
})
|
|
/* WER verdeckt WEN, und wo. Vorher stand hier nur der Name des
|
|
verdeckten Knopfes -- eine Meldung wie `BUTTON"Meine Sicht"` sagt,
|
|
DASS etwas nicht stimmt, und laesst einen danach raten, WAS
|
|
darueberliegt. Ich habe daran am 06.09.2026 eine Stunde gesucht.
|
|
Ein Befund, den man nicht verorten kann, ist ein halber Befund. */
|
|
.map((e) => {
|
|
const r = e.getBoundingClientRect();
|
|
const oben = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
|
|
const kette = (x) => {
|
|
const a = [];
|
|
for (let v = x; v && a.length < 4; v = v.parentElement) {
|
|
a.push(v.tagName + (v.className && typeof v.className === "string"
|
|
? "." + v.className.split(" ")[0] : ""));
|
|
}
|
|
return a.join("<");
|
|
};
|
|
const q = oben ? oben.getBoundingClientRect() : null;
|
|
return `${kette(e)} "${(e.textContent || "").trim().slice(0, 16)}" `
|
|
+ `[${Math.round(r.left)},${Math.round(r.top)} ${Math.round(r.width)}x${Math.round(r.height)}]`
|
|
+ ` <- verdeckt von ${oben ? kette(oben) : "?"}`
|
|
+ (q ? ` [${Math.round(q.left)},${Math.round(q.top)} ${Math.round(q.width)}x${Math.round(q.height)}]` : "");
|
|
})
|
|
.slice(0, 3),
|
|
|
|
/* DIE GEOMETRIE DER KOPFLEISTE, aber nur wenn etwas nicht stimmt.
|
|
|
|
Am 06.09.2026 meldete diese Pruefung auf zwoelf Seiten "BUTTON
|
|
'Meine Sicht' verdeckt" -- und mehr nicht. Ich habe danach eine
|
|
Stunde gesucht, weil aus der Meldung nicht hervorging, WAS
|
|
darueberlag und bei WELCHER Breite. Erst dieser Auszug zeigte es in
|
|
einer Zeile: bei 412 px stand `wrap=nowrap`, der Umschalter war auf
|
|
50 px zusammengedrueckt, sein Knopf 104 px breit.
|
|
|
|
Bleibt deshalb drin. Ein Befund, den man nicht verorten kann, ist
|
|
ein halber Befund -- und er kostet beim naechsten Mal wieder eine
|
|
Stunde. */
|
|
kopfDump: (() => {
|
|
const k = document.querySelector(".kopfleiste__rechts");
|
|
const l = document.querySelector(".kopfleiste");
|
|
if (!k || !l) return null;
|
|
const box = (e) => { const q = e.getBoundingClientRect();
|
|
return `${Math.round(q.left)},${Math.round(q.top)} ${Math.round(q.width)}x${Math.round(q.height)}`; };
|
|
const m = document.querySelector(".kopfleiste > .marke");
|
|
return {
|
|
w: window.innerWidth,
|
|
leiste: box(l) + " wrap=" + getComputedStyle(l).flexWrap,
|
|
rechts: box(k) + " wrap=" + getComputedStyle(k).flexWrap,
|
|
marke: m ? box(m) + " flex=" + getComputedStyle(m).flex : "keine",
|
|
kinder: [...k.children].map((e) => `${e.tagName}.${String(e.className||"").split(" ")[0]} ${box(e)}`),
|
|
};
|
|
})(),
|
|
|
|
/* 5. Die Kopfleiste muss vollstaendig im Bild sein -- sie ist die
|
|
einzige Stelle, ueber die man wieder wegkommt. */
|
|
kopfDrin: (() => {
|
|
const a = document.querySelector(".abmelden")?.getBoundingClientRect();
|
|
return a ? (a.right <= window.innerWidth + 1 && a.left >= -1) : null;
|
|
})(),
|
|
/* 6. Schriftgroesse: unter 12 px liest niemand auf einem Handy. */
|
|
/* NUR SICHTBARER TEXT. Ein ausgeblendetes Element hat weiterhin eine
|
|
Schriftgroesse -- die Pruefung meldete deshalb ein Schild, das auf
|
|
dem Handy per display:none gar nicht erscheint. Eine Pruefung, die
|
|
Unsichtbares anmahnt, gewoehnt man sich ab zu lesen. */
|
|
winzig: [...document.querySelectorAll("p, span, li, a, label, small")]
|
|
.filter((e) => {
|
|
if (e.children.length || e.textContent.trim().length <= 3) return false;
|
|
const r = e.getBoundingClientRect();
|
|
if (r.width <= 0 || r.height <= 0) return false;
|
|
const st = getComputedStyle(e);
|
|
/* `font-size: 0` IST KEINE KLEINE SCHRIFT, SONDERN KEINE.
|
|
|
|
Im Monatsraster des Kalenders wird aus jedem Termin ein
|
|
farbiger Punkt: Die Schrift wird auf 0 gesetzt, die Farbe
|
|
bleibt. Der Text ist damit nicht winzig, sondern bewusst
|
|
nicht dargestellt -- gelesen wird er im Tagesdialog. Ihn als
|
|
„0px, zu klein" zu melden, schickt einen an eine Stelle, an
|
|
der genau das beabsichtigt ist.
|
|
|
|
DIE GRENZE NACH UNTEN BLEIBT SCHARF: Alles zwischen 0,1 und
|
|
11,5 px ist weiterhin ein Befund. Nur die glatte Null faellt
|
|
heraus, und die ist eine Aussage, keine Nachlaessigkeit. */
|
|
const g = parseFloat(st.fontSize);
|
|
if (!(g > 0)) return false;
|
|
return g < 11.5;
|
|
})
|
|
.map((e) => `"${e.textContent.trim().slice(0, 18)}" ${getComputedStyle(e).fontSize}`)
|
|
.slice(0, 3),
|
|
});
|
|
|
|
for (const [breite, hoehe, wie] of GERAETE) {
|
|
console.log(`\n=== ${wie} (${breite}x${hoehe})`);
|
|
const kontext = await browser.newContext({
|
|
viewport: { width: breite, height: hoehe },
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true,
|
|
});
|
|
const seite = await kontext.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
|
|
/* Auch die Anmeldeseite selbst wird geprueft -- sie ist die erste,
|
|
die jemand auf dem Handy sieht. */
|
|
{
|
|
await seite.waitForTimeout(500);
|
|
const r = await seite.evaluate(messen);
|
|
const gut = r.ueber <= 0 && !r.abgeschnitten.length && !r.klein.length && !r.verdeckt.length;
|
|
ok(gut, `Anmeldung über=${r.ueber}px abgeschnitten=${r.abgeschnitten.length} `
|
|
+ `klein=${r.klein.length} verdeckt=${r.verdeckt.length}`
|
|
+ (r.schuld.length ? `\n steht über: ${r.schuld.join(", ")}` : "")
|
|
+ (r.abgeschnitten.length ? `\n abgeschnitten: ${r.abgeschnitten.join(", ")}` : "")
|
|
+ (r.klein.length ? `\n zu klein: ${r.klein.join(", ")}` : "")
|
|
+ (r.verdeckt.length ? `\n verdeckt: ${r.verdeckt.join(", ")}` : ""));
|
|
}
|
|
|
|
await seite.click('.rolle[data-rolle="admin"]');
|
|
await seite.fill("#code", "CODE-DOGI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
|
|
|
for (const s of SEITEN) {
|
|
const konsole = [];
|
|
const auf = (m) => { if (m.type() === "error") konsole.push(m.text()); };
|
|
const err = (e) => konsole.push("pageerror: " + e.message);
|
|
seite.on("console", auf);
|
|
seite.on("pageerror", err);
|
|
|
|
await seite.goto(BASIS + "/workspace/" + s, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(700);
|
|
const r = await seite.evaluate(messen);
|
|
|
|
const gut = r.ueber <= 0 && !r.abgeschnitten.length && !r.klein.length
|
|
&& !r.verdeckt.length && !r.winzig.length
|
|
&& r.kopfDrin !== false && !konsole.length;
|
|
|
|
console.log(` ${gut ? "ok " : "FEHL"} ${String(s).padEnd(20)}`
|
|
+ `über=${String(r.ueber).padStart(3)}px`
|
|
+ (r.schuld.length ? `\n steht über: ${r.schuld.join(", ")}` : "")
|
|
+ (r.abgeschnitten.length ? `\n abgeschnitten: ${r.abgeschnitten.join(", ")}` : "")
|
|
+ (r.klein.length ? `\n Berührziel zu klein: ${r.klein.join(", ")}` : "")
|
|
+ (r.verdeckt.length ? `\n verdeckt: ${r.verdeckt.join(", ")}` : "")
|
|
+ (r.winzig.length ? `\n zu kleine Schrift: ${r.winzig.join(", ")}` : "")
|
|
+ (r.kopfDrin === false ? "\n der Abmelden-Knopf steht aus dem Bild" : "")
|
|
+ (konsole.length ? `\n KONSOLE: ${konsole.join(" | ")}` : "")
|
|
+ (!gut && r.kopfDump ? `\n KOPF: ${JSON.stringify(r.kopfDump)}` : ""));
|
|
if (!gut) fehler++;
|
|
|
|
seite.off("console", auf);
|
|
seite.off("pageerror", err);
|
|
}
|
|
|
|
if (breite === 390) {
|
|
await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(600);
|
|
await seite.screenshot({ path: "pruef-handy.png", fullPage: true });
|
|
}
|
|
await kontext.close();
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Bedienbarkeit mit dem Finger: Auf einem Geraet ohne feinen Zeiger darf
|
|
das Licht gar nicht erst eingebaut werden -- es kostete Arbeit fuer
|
|
einen Effekt, den dort niemand sehen kann.
|
|
--------------------------------------------------------------------- */
|
|
console.log("\n=== Kein Licht ohne Zeiger");
|
|
{
|
|
const kontext = await browser.newContext({
|
|
viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
|
|
const seite = await kontext.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click('.rolle[data-rolle="admin"]');
|
|
await seite.fill("#code", "CODE-DOGI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
|
await seite.waitForTimeout(800);
|
|
/* Eine Kachel antippen und wieder loslassen -- danach darf nichts
|
|
leuchten bleiben. */
|
|
await seite.tap(".kachel >> nth=1").catch(() => {});
|
|
await seite.waitForTimeout(400);
|
|
const r = await seite.evaluate(() => ({
|
|
lichter: document.querySelectorAll(".licht").length,
|
|
an: document.querySelectorAll('[data-licht="an"]').length,
|
|
}));
|
|
ok(r.lichter === 0, `auf dem Handy wird gar kein Licht eingebaut (${r.lichter})`);
|
|
ok(r.an === 0, `und keines bleibt an (${r.an})`);
|
|
await kontext.close();
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
RING UND UHR STEHEN IN DER MITTE -- bei jeder Rolle (09.09.2026)
|
|
|
|
Filipe, mit Bildschirmfoto: "ich will dass auf dem handy die uhr und
|
|
der kreis ganz oben mit den erledigten aufgaben, schoen mittig,
|
|
zentriert sind. die stehen sehr verzogen von der mitte. das soll bei
|
|
jedem und jeder rolle verbessert werden."
|
|
|
|
Gemessen war es bei allen fuenf Rollen und beiden Breiten dieselbe
|
|
Zahl: Ring 31 px nach links, Uhr 31 px nach rechts. Ursache war
|
|
`justify-content: center` auf einer Reihe, in der neben dem
|
|
Instrument noch ein Knopf steht -- zentriert wurde der Block aus
|
|
beiden. 62 px Knopfbreite (`--instrument-knoepfe`), halbiert, macht
|
|
genau die 31.
|
|
|
|
Warum das hier ALLE Rollen durchgeht, obwohl die Ursache eine einzige
|
|
CSS-Regel ist: Weil genau das die Behauptung war, die geprueft
|
|
gehoert. Eine Regel kann von einer rollenabhaengigen ueberschrieben
|
|
werden, und dann faellt es nur bei einer auf.
|
|
--------------------------------------------------------------------- */
|
|
/* EINE Messung, zweimal benutzt (eigene und fremde Sicht). Zwei Kopien
|
|
waeren zwei Kopien, die auseinanderlaufen -- und dann prueft die eine
|
|
etwas anderes als die andere, ohne dass es auffaellt.
|
|
|
|
Gemessen wird die ZEILENZAHL, nicht die Hoehe: Eine Hoehengrenze waere
|
|
wieder eine Zahl von gestern -- sie stimmt, bis jemand die Polsterung
|
|
anfasst. Ob zwei Dinge in derselben Zeile stehen, sieht man an ihrer
|
|
Oberkante, und das gilt immer. */
|
|
async function kopfMessen(seite) {
|
|
return await seite.evaluate(() => {
|
|
const k = document.querySelector(".kopfleiste");
|
|
const teile = [...k.querySelectorAll(".kopfleiste__rechts > *"), k.querySelector(".marke")]
|
|
.filter((e) => e && e.getBoundingClientRect().width > 0);
|
|
const kanten = teile.map((e) => Math.round(e.getBoundingClientRect().top));
|
|
return { zeilen: new Set(kanten).size, spanne: Math.max(...kanten) - Math.min(...kanten),
|
|
anzahl: teile.length,
|
|
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth };
|
|
});
|
|
}
|
|
let fremdGeprueft = 0;
|
|
let weckerGeprueft = 0;
|
|
const mitUmschalter = new Set();
|
|
const KOPF_GERAETE = [[360, 800], [390, 844], [412, 915]];
|
|
|
|
console.log("\n=== Ring und Uhr stehen mittig (alle Rollen)");
|
|
for (const [breite, hoehe] of KOPF_GERAETE) {
|
|
for (const [rolle, code] of [["admin", "CODE-DOGI-0001"], ["spicy", "CODE-SPIC-0001"],
|
|
["manager", "CODE-MANA-0001"], ["scout", "CODE-SCOU-0001"], ["creator", "CODE-ANNA-0001"]]) {
|
|
const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe }, hasTouch: breite <= 860 });
|
|
const seite = await kontext.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
|
await seite.fill("#code", code);
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
|
await seite.waitForTimeout(1500);
|
|
const m = await seite.evaluate(() => {
|
|
const ab = (w) => {
|
|
const e = document.querySelector(w);
|
|
if (!e) return null;
|
|
const r = e.getBoundingClientRect();
|
|
if (r.width < 4) return null;
|
|
return Math.round((r.left + r.right) / 2 - window.innerWidth / 2);
|
|
};
|
|
return { ring: ab("#zring"), uhr: ab(".uhr"),
|
|
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth };
|
|
});
|
|
/* 3 px Nachsicht: Ein ungerader Rest bei ungerader Breite ist kein
|
|
Befund. 31 px waren einer. */
|
|
const gut = m.ring !== null && m.uhr !== null
|
|
&& Math.abs(m.ring) <= 3 && Math.abs(m.uhr) <= 3 && m.ueber <= 1;
|
|
ok(gut, `${breite}px ${rolle.padEnd(8)} Ring ${m.ring} px, Uhr ${m.uhr} px von der Mitte`
|
|
+ (m.ueber > 1 ? `, Ueberstand ${m.ueber}` : ""));
|
|
|
|
/* ---- UND DIE KOPFLEISTE BLEIBT IN EINER REIHE (09.09.2026) ------
|
|
Filipe, mit Bildschirmfoto: "es soll alles in einer reihe sein."
|
|
Auf seinem Bild stand "Abmelden" allein in einer zweiten Zeile.
|
|
|
|
Gemessen wird die ZEILENZAHL, nicht die Hoehe: Eine Hoehengrenze
|
|
waere wieder eine Zahl von gestern -- sie stimmt, bis jemand die
|
|
Polsterung anfasst. Ob zwei Dinge in derselben Zeile stehen,
|
|
sieht man an ihrer Oberkante, und das gilt immer. */
|
|
const kopf = await kopfMessen(seite);
|
|
/* 12 px Nachsicht: Ein Knopf von 36 px und ein Bild von 28 px sind
|
|
mittig ausgerichtet und haben deshalb verschiedene Oberkanten --
|
|
das ist dieselbe Zeile. Eine zweite Zeile liegt 40 px tiefer. */
|
|
ok(kopf.anzahl >= 3 && kopf.spanne <= 12 && kopf.ueber <= 1,
|
|
`${breite}px ${rolle.padEnd(8)} Kopfleiste: ${kopf.anzahl} Teile in einer Reihe `
|
|
+ `(Spanne ${kopf.spanne} px)`);
|
|
|
|
/* ---- DAS ZEITFELD DES WECKERS STEHT IM BILD (09.09.2026) -------
|
|
|
|
Filipe: "wenn ich am handy auf den wecker drücke dan sieht man
|
|
nichts." Es ging auf -- nur ausserhalb des Bildschirms, bei
|
|
390 px von -92 bis 27. `.tagesruf__feld` oeffnet nach LINKS, und
|
|
auf dem Handy ist der Knopf ganz links.
|
|
|
|
Geprueft wird aufgeklappt, nicht zugeklappt: Ein verstecktes Feld
|
|
hat keine brauchbare Lage, und genau die ist die Frage. Deshalb
|
|
wird es hier von Hand aufgemacht -- was beim Einschalten sonst
|
|
das Skript tut. */
|
|
const wecker = await seite.evaluate(() => {
|
|
const feld = document.querySelector(".tagesruf__feld");
|
|
if (!feld) return null;
|
|
const vorher = feld.hidden;
|
|
feld.hidden = false;
|
|
const r = feld.getBoundingClientRect();
|
|
feld.hidden = vorher;
|
|
return { links: Math.round(r.left), rechts: Math.round(r.right),
|
|
breit: Math.round(r.width), fenster: window.innerWidth };
|
|
});
|
|
if (wecker) {
|
|
weckerGeprueft++;
|
|
ok(wecker.breit > 40 && wecker.links >= -1 && wecker.rechts <= wecker.fenster + 1,
|
|
`${breite}px ${rolle.padEnd(8)} Weckerfeld liegt bei `
|
|
+ `${wecker.links}..${wecker.rechts} von ${wecker.fenster}`);
|
|
}
|
|
|
|
/* ---- UND DASSELBE IN EINER FREMDEN SICHT (09.09.2026) ----------
|
|
|
|
DIESE PRUEFUNG FEHLTE, UND GENAU DAS HAT DER FEHLER AUSGENUTZT.
|
|
|
|
Filipe meldete aus der installierten App "es ist noch nicht alles
|
|
in einer zeile" -- waehrend hier alles gruen war. Der Grund: Alle
|
|
Messungen liefen in der EIGENEN Sicht. Dort klappt der Umschalter
|
|
auf 36 px zusammen; sobald man die Sicht einer anderen Person
|
|
uebernimmt, stand der Name darin und er war 152 px breit. Auf
|
|
320, 360, 390, 412 UND 430 px waren es dann zwei Zeilen.
|
|
|
|
Ein gruener Haken sagt nur, dass die Bedingung erfuellt war --
|
|
nicht, dass sie den Zustand geprueft hat, in dem der Nutzer ist.
|
|
|
|
Wer keinen Umschalter hat (Creator, Scout ...), wird nicht
|
|
uebersprungen, sondern gar nicht erst gezaehlt; dass ueberhaupt
|
|
jemand geprueft wurde, steht unten in einer eigenen Bedingung. */
|
|
const wenAnsehen = await seite.evaluate(() => {
|
|
const s = document.getElementById("kopf-sicht-feld");
|
|
if (!s) return null;
|
|
const o = [...s.options].find((x) => x.value);
|
|
return o ? { id: o.value, name: o.textContent } : null;
|
|
});
|
|
if (wenAnsehen) {
|
|
await seite.evaluate((x) => localStorage.setItem("dfw-sicht", x), wenAnsehen.id);
|
|
await seite.reload({ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1400);
|
|
const fremd = await kopfMessen(seite);
|
|
const band = await seite.evaluate(() => {
|
|
const b = document.querySelector(".fremd-band");
|
|
if (!b) return { da: false };
|
|
const r = b.getBoundingClientRect();
|
|
return { da: r.width > 0 && r.height > 0, text: (b.textContent || "").trim().slice(0, 60) };
|
|
});
|
|
fremdGeprueft++;
|
|
mitUmschalter.add(rolle);
|
|
ok(fremd.anzahl >= 3 && fremd.spanne <= 12 && fremd.ueber <= 1,
|
|
`${breite}px ${rolle.padEnd(8)} fremde Sicht: ${fremd.anzahl} Teile in einer Reihe `
|
|
+ `(Spanne ${fremd.spanne} px)`);
|
|
/* Der Name darf nicht einfach verschwinden -- er ist aus der
|
|
Leiste ins Band gewandert, und dass er DORT steht, gehoert
|
|
geprueft. Sonst waere "eine Reihe" damit erkauft, dass niemand
|
|
mehr sieht, wessen Zahlen er ansieht. */
|
|
ok(band.da && band.text.includes(wenAnsehen.name.trim()),
|
|
`${breite}px ${rolle.padEnd(8)} das Band nennt den vollen Namen`);
|
|
await seite.evaluate(() => localStorage.removeItem("dfw-sicht"));
|
|
}
|
|
await kontext.close();
|
|
}
|
|
}
|
|
|
|
/* Die Zahl gehoert in die Bedingung, nicht nur in den Bericht: Findet
|
|
die Schleife keinen einzigen Umschalter (umbenannt, Rechte geaendert),
|
|
waere oben alles gruen, ohne dass die fremde Sicht je gemessen wurde.
|
|
|
|
Die Erwartung wird ABGELEITET, nicht hingeschrieben: so viele Rollen
|
|
mit Umschalter, wie es gibt, mal so viele Breiten, wie geprueft
|
|
werden. Heute hat nur DogFather einen -- bekommt Spicy morgen auch
|
|
einen, steigt die Zahl von selbst mit, statt still danebenzuliegen.
|
|
(Beim ersten Anlauf stand hier eine 6, geraten; gemessen waren es 3.
|
|
Genau die Sorte Zahl, die spaeter grundlos rot wird.) */
|
|
ok(mitUmschalter.size >= 1 && fremdGeprueft === KOPF_GERAETE.length * mitUmschalter.size,
|
|
`die fremde Sicht wurde ${fremdGeprueft}-mal gemessen `
|
|
+ `(${mitUmschalter.size} Rolle(n) mit Umschalter x ${KOPF_GERAETE.length} Breiten)`);
|
|
|
|
/* Auch hier: Findet die Schleife den Wecker gar nicht (umbenannt, an
|
|
eine andere Stelle gewandert, Push nicht moeglich), waere oben alles
|
|
gruen, ohne dass er je gemessen wurde. */
|
|
ok(weckerGeprueft >= KOPF_GERAETE.length,
|
|
`das Weckerfeld wurde ${weckerGeprueft}-mal wirklich gemessen`);
|
|
|
|
/* =====================================================================
|
|
GEGENPROBE ZUR BERUEHRZIEL-REGEL (14.09.2026)
|
|
|
|
Seit heute hat die Regel eine Ausnahme (Links im Satz, WCAG 2.5.8).
|
|
Eine Regel mit Ausnahme braucht den Beweis in BEIDE Richtungen --
|
|
sonst laesst sich nicht unterscheiden, ob sie nichts findet, weil
|
|
nichts da ist, oder weil die Ausnahme alles verschluckt.
|
|
|
|
Diese Datei hatte bis heute ueberhaupt keine Gegenprobe. Sie konnte
|
|
also nie zeigen, dass sie ein zu kleines Ziel ueberhaupt bemerkt.
|
|
===================================================================== */
|
|
console.log("\n=== Gegenprobe zur Beruehrziel-Regel ===");
|
|
{
|
|
const kontext = await browser.newContext({
|
|
viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
|
|
const seite = await kontext.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await seite.evaluate(() => {
|
|
const k = document.createElement("div");
|
|
k.id = "gegenprobe";
|
|
/* Zweimal dieselbe Hoehe: einmal allein stehend, einmal im Satz. */
|
|
k.innerHTML =
|
|
"<a href='#a' id='g-allein' style='display:block;width:20px;height:20px'>xx</a>"
|
|
+ "<p>Das hier ist ein ganz normaler Satz mit einem "
|
|
+ "<a href='#b' id='g-satz' style='font-size:13px;line-height:20px'>Verweis darin</a>"
|
|
+ " und noch etwas Text dahinter.</p>";
|
|
document.body.append(k);
|
|
});
|
|
const r = await seite.evaluate(messen);
|
|
const namen = r.klein.join(" | ");
|
|
ok(/20x20/.test(namen),
|
|
`ein allein stehendes 20x20-Ziel wird erkannt (${namen || "nichts gefunden"})`);
|
|
ok(!/Verweis darin/.test(namen),
|
|
"ein gleich grosser Verweis IM SATZ wird zu Recht nicht gemeldet (WCAG 2.5.8)");
|
|
await seite.evaluate(() => document.getElementById("gegenprobe")?.remove());
|
|
await kontext.close();
|
|
}
|
|
await browser.close();
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung.");
|
|
process.exit(fehler ? 1 : 0);
|