Filipe, mit Bildschirmfoto: "ich will dass es ueberall perfektionniert
wird. ich will dass du das richtig geil machst es soll alles in einer
reihe sein." Auf dem Bild stand "Abmelden" allein in einer zweiten
Zeile, darueber Teilen, Chat, Suche und das Profilbild.
--- GEMESSEN, BEVOR ETWAS GEAENDERT WURDE ---
Bei 390 px, alle fuenf Rollen:
verfuegbar fuer die rechte Gruppe 275 px
gebraucht als DogFather 393 px
(Sicht 150 · Teilen 38 · Chat 37 · Suche 47 · Bild 28 · Abmelden 93)
gebraucht als Scout/Manager/Spicy 243 px
gebraucht als Creator 205 px
Der Umbruch war also die richtige Antwort auf ein echtes
Platzproblem. `flex-wrap` misst und rechnet nicht -- das bleibt so.
Geaendert wird der PLATZBEDARF, nicht die Reaktion darauf. Eine
Schwelle, ab der nicht mehr umgebrochen wird, waere wieder eine
Rechnung von gestern: Beim naechsten Knopf staende alles uebereinander.
--- WAS SCHRUMPFT, UND WAS NICHT ---
Die drei Zeichen-Knoepfe waren 38, 37 und 47 px breit -- nebeneinander
sah das unruhig aus. Jetzt alle 36.
"Abmelden" wird auf schmalen Bildschirmen zum Zeichen (93 -> 36). Das
Wort bleibt im aria-label und am Rechner sichtbar: Dort ist Platz, und
ein Wort ist eindeutiger als ein Bild. Der Aufbau steht in kopf.js und
nicht in neunzehn HTML-Dateien -- neunzehn Stellen sind achtzehn
Gelegenheiten, eine zu vergessen.
Der Sicht-Umschalter wird ein Zeichen-Knopf wie die anderen (150 -> 36),
ABER NUR solange die eigene Sicht laeuft. Bei einer fremden behaelt er
den Namen, und dann darf die Leiste auch umbrechen: Dass man fremde
Zahlen ansieht, ist wichtiger als eine gerade Zeile. Das ist der
gefaehrlichste Fall dieser Funktion, und er bleibt unangetastet.
--- ZWEI DINGE, DIE ERST DIE MESSUNG GEZEIGT HAT ---
1. `max-width: 30px` am Umschalter-Kasten wirkte NICHT. Gemessen stand
da: berechnete Hoechstbreite 30 px, tatsaechliche Breite 104. Der
Knopf DARIN behielt seine Groesse und schob den Kasten wieder auf.
Wer nur den Rahmen begrenzt, begrenzt nichts -- die Breite kommt vom
Inhalt. Jetzt liegt der Knopf unsichtbar UEBER dem Auge: die ganze
Flaeche ist das Ziel, der Fokusring bleibt, die Breite ist 36.
2. Bei 360 px brach es weiter um, obwohl es rechnerisch passte. Ursache
war eine eigene Regel bei 380 px, die Marke und Bedienelemente
ausdruecklich in zwei Zeilen zwang -- richtig, solange die
Bedienelemente 243 bis 393 px brauchten, falsch seit sie 136 bis 238
brauchen. Die Schwelle liegt jetzt bei 300 px; darunter gibt es
Geraete, auf denen es wirklich nicht reicht.
--- Pruefung ---
pruef-handy, 15 neue Messungen (fuenf Rollen x 360/390/412): alle Teile
der Kopfleiste stehen in einer Reihe, Spanne 4 px.
Gemessen wird die ZEILENZAHL ueber die Oberkanten, nicht die Hoehe der
Leiste. 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.
Ausserdem gruen: pruef-css-klassen.
Co-Authored-By: Claude Opus 5 <[email protected]>
515 lines
26 KiB
JavaScript
515 lines
26 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");
|
|
process.env.PORT = "4198";
|
|
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";
|
|
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:4198";
|
|
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;
|
|
return e.scrollWidth > e.clientWidth + 2;
|
|
})
|
|
.map((e) => `"${e.textContent.trim().slice(0, 22)}" (${e.scrollWidth}>${e.clientWidth})`)
|
|
.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. */
|
|
klein: [...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;
|
|
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;
|
|
return parseFloat(getComputedStyle(e).fontSize) < 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.
|
|
--------------------------------------------------------------------- */
|
|
console.log("\n=== Ring und Uhr stehen mittig (alle Rollen)");
|
|
for (const [breite, hoehe] of [[360, 800], [390, 844], [412, 915]]) {
|
|
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 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 };
|
|
});
|
|
/* 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)`);
|
|
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);
|