Drei Bildschirmfotos, drei Befunde -- und alle drei waren messbar. --- SCREEN 2: RING UND UHR STANDEN SCHIEF --- Filipe: "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 bei 390 und 412 px, in allen fuenf Rollen, immer dieselbe Zahl: Ring 31 px nach links, Uhr 31 px nach rechts. Gleicher Betrag, entgegengesetzte Richtung -- eine Ursache, nicht zwei. `justify-content: center` zentriert den INHALT der Reihe, und in der Reihe steht neben dem Instrument noch ein Knopf (Glocke rechts vom Ring, Wecker links von der Uhr). Zentriert wurde also [Knopf + Abstand + Instrument] als Block; das Instrument rutschte um die halbe Knopfbreite zur Seite. Die Knopfbreite steht seit jeher als `--instrument-knoepfe: 62px` in derselben Datei -- 62 / 2 = 31. Die gemessene Zahl war von Anfang an aufgeschrieben, nur an anderer Stelle. Der Knopf zaehlt jetzt nicht mehr mit: aus dem Fluss genommen, neben das zentrierte Instrument gehaengt. NICHT um 31 px verschoben -- das waere dieselbe Zahl ein zweites Mal, und beim naechsten Knopf waere sie falsch. Jetzt folgt seine Lage dem Instrument, wie gross das auch ist. Nachher: 0 px Abweichung, alle fuenf Rollen, beide Breiten. --- SCREEN 3: DAS BABYBLAU WAR STAHLGRAU --- Filipe: "die rolle dogfather soll eine uebertrieben geile babyblau haben." Zum dritten Mal -- und die ersten beiden Male habe ich das Falsche vergroessert. Beide Male hatte ich den Abstand zwischen Rot- und Blaukanal im HEXWERT erhoeht (45 Stufen, dann 86). Diesmal zuerst gemessen, was am Bildschirm ankommt: Die DogFather-Zeile traegt 98,7 % farbige Bildpunkte -- MEHR als jede andere Rolle. An der Menge lag es also nie. Ihr Mittelwert war rgb(43,62,78): 35 Stufen zwischen Rot und Blau. Das ist Stahlgrau. Die richtige Groesse ist die SAETTIGUNG. In OKLab hatte #9ed3f4 eine Buntheit von 0,0718 -- der blasseste Wert aller fuenf Rollen. #5fbdff hat 0,1303, also 81 Prozent mehr. Die Zeile kommt damit auf rgb(36,60,79), und der mittlere Kanalabstand steigt von 35,2 auf 43,0. Nachgerechnet, was NICHT verlorengeht: Kontrast auf dem Grund 9,75:1 (vorher 12,49) -- weit ueber den 7 fuer AAA. Abstand zur naechsten Rollenfarbe 0,1608 in OKLab, vorher 0,1466; die Hausgrenze liegt bei 0,0973. DogFather ist also SICHERER unterscheidbar als vorher. Gemessen an der echten Flaeche: 8,84:1 und 9,60:1. --- SCREEN 4: SPICY FEHLTE IM UMSCHALTER --- Filipe: "ich will da auch noch die spicy rolle sehen und die leute in der rolle wie die anderen." Der Sicht-Umschalter fuehrte eine EIGENE Rollenliste -- vier Eintraege, Spicy Media fehlte. Zum vierten Mal derselbe Fehler: eine zweite Fassung einer Liste, die es zentral schon gibt. Vorher traf es die Leitungsliste (ein Set), die Rollennamen (ein Objekt in fuenf Skripten -- im Chat stand "UNDEFINED" ueber einem Namen) und die Personenliste. Jetzt steht dort keine Liste mehr, sondern eine Ableitung aus `ROLLENFOLGE` und `ROLLEN_GRUPPE`. Kommt eine sechste Rolle, ist sie ohne eine Zeile Arbeit dabei. --- Pruefung --- pruef-handy: zehn neue Messungen (fuenf Rollen x zwei Breiten), alle 0 px. Die beiden fehlenden Rollen sind dafuer in den Testdaten ergaenzt -- eine Pruefung an drei von fuenf haette "bei jeder Rolle" nicht belegen koennen. pruef-sicht: Cigdem als Spicy-Media-Person ergaenzt. Ohne sie waere es nie aufgefallen -- eine Rolle ohne Leute wird ohnehin weggelassen, und die alte Zusicherung "Manager, Scouts, Creator" waere weiter gruen gewesen. Dazu eine Gegenprobe, dass die Reihenfolge WIRKLICH aus der zentralen Liste kommt und nicht wieder abgeschrieben ist. pruef-personen-kachel: DogFather-Kontrast neu dabei. Ausserdem gruen: pruef-css-klassen, pruef-lesbarkeit. Co-Authored-By: Claude Opus 5 <[email protected]>
491 lines
24 KiB
JavaScript
491 lines
24 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 [[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}` : ""));
|
|
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);
|