Weiter am Rundumcheck, diesmal der Handy-Rundgang. Von sechs Befunden war einer ein echter Layoutfehler, zwei waren zu kleine Schrift, und drei kamen daher, dass die Messung etwas nicht unterscheiden konnte. === AUF DEM HANDY KAPUTT === 1. DER NAME IN DER PERSONENKARTE WAR NULL PIXEL BREIT. Gemessen auf 412 px: `h3.tperson__name` mit `w=0` bzw. `w=15`. Der Name stand als Buchstabensaeule da oder gar nicht -- auf der Seite, die von Menschen handelt. `.tperson__text` trug `flex: 1; min-width: 0`. Das erlaubt dem Textblock, auf null zu schrumpfen, und Flexbox schrumpft lieber, als umzubrechen -- die Pille „zuletzt gesehen" daneben blieb stehen und nahm allen Platz. `min-width: 0` war trotzdem richtig gemeint (ohne sie blaeht ein langes Wort den Kasten auf); es fehlte nur die Untergrenze. Jetzt `min(14ch, 100%)`: vierzehn Zeichen, wenn so viel Platz ist, sonst der ganze Platz, der da ist. Die Pille bricht um -- `flex-wrap: wrap` stand am Kopf ohnehin schon, es fehlte nur der Grund, es zu benutzen. 2. ZWEI BESCHRIFTUNGEN UNTER DER LESBARKEITSGRENZE. `.u-weg__marke` 10,88 px, `.u-weg__aus` 11,2 px -- die Hausgrenze sind 11,5. Der Reflex dahinter: Eine Marke soll leise sein, also macht man sie klein. Leise wird sie aber durch Farbe und Gewicht; eine Schrift, die man nicht lesen kann, ist nicht leise, sondern weg. Derselbe Griff ist mir gestern dreimal an einem Tag passiert. === DREI MESSUNGEN, DIE ETWAS NICHT UNTERSCHEIDEN KONNTEN === 3. `pointer-events: none` IST KEIN BERUEHRZIEL. Die Terminpunkte im Monatsraster des Kalenders sind 8 x 8 px und nehmen ausdruecklich keine Beruehrung an -- angetippt wird die ZELLE. Sie als „zu klein" zu melden ist, als beanstande man die Groesse eines gemalten Knopfs. `pruef-breiten` kennt die Ausnahme seit jeher; im Handy-Rundgang hat sie gefehlt. 4. `font-size: 0` IST KEINE KLEINE SCHRIFT, SONDERN KEINE. Dieselben Punkte: Die Schrift wird auf null gesetzt, die Farbe bleibt. Gemeldet wurde „0px, zu klein". Die Grenze nach unten bleibt scharf -- alles zwischen 0,1 und 11,5 px ist weiterhin ein Befund, nur die glatte Null faellt heraus. Sie ist eine Aussage, keine Nachlaessigkeit. 5. `scrollWidth > clientWidth` SAGT BEI INLINE-ELEMENTEN NICHTS. Chromium liefert dort fuer `clientWidth` glatt null, und damit ist jeder Text breiter als sein Kasten. Der richtige Umgang mit einer unmoeglichen Messung ist, sie nicht zu machen -- nicht, ihr Ergebnis zu glauben. Dazu: Was per `clip-path: inset(50%)` fuer das Auge weggenommen ist (echte <select> unter selbst gebauten Umschaltern, Beschriftungen zu Symbolknoepfen), kann nicht abgeschnitten sein. === UND EINE MELDUNG, DIE JETZT SAGT, WO MAN SUCHEN MUSS === „abgeschnitten: Mara (18>0)" hat mich zwanzig Minuten gekostet -- drei Vermutungen, drei Messungen. Die Meldung nennt jetzt Element, Klasse, Darstellungsart und Breite: „Mara (18>0, h3.tperson__name, block, w=0)". Damit war der Fall in einem Blick klar. Eine Pruefung, die nur sagt DASS etwas ist, ist eine halbe. GEMESSEN: pruef-breiten 0 Fehler (9 Breiten, 38 Seiten), pruef-team 51/0, pruef-tippziele 11/0, pruef-css-klassen 33/0. Im Handy-Rundgang bleiben die Kopfleisten-Befunde bei 360/390/412 px -- die nehme ich mir als Naechstes vor, sie brauchen einen Umbau und keine Korrektur. 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 } });
|
|
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);
|