Filipe: "ich will dass du die komplette seite auf dem handy abcheckst.
ich will dass alles perfekt aussieht und bedienbar ist."
DER SCHWERSTE FUND: Auf ZWOELF Seiten war der "Meine Sicht"-Umschalter
nicht bedienbar -- wer darauf tippte, landete im Chat.
Bei 412 px (der haeufigsten Android-Breite ueberhaupt) brach die
Kopfleiste nicht um, der Umschalter wurde auf 50 px zusammengedrueckt,
sein Knopf behielt aber seine 104 px Mindestbreite und lag damit quer
ueber dem Chat-Knopf. Sichtbar war davon nichts.
Und die Ursache steht seit dem 05.09. woertlich im Kommentar daneben:
"Die Rechnung ging genau auf, solange rechts VIER Dinge standen. Mit der
Glocke sind es FUENF, und bei 320 px passte es nicht mehr." Am 06.09.
habe ich den Chat-Knopf dazugesetzt -- SECHS -- und die Schwelle bei 380
gelassen. Derselbe Fehler, eine Position weiter.
Die Lehre ist nicht "380 auf 430 erhoehen"; das waere er ein drittes
Mal, nur mit einer anderen Zahl. Eine feste Schwelle ist eine Rechnung,
die jemand einmal aufgestellt hat und die beim naechsten Knopf still
falsch wird. `flex-wrap: wrap` OHNE Schwelle rechnet nicht, sondern
misst -- es bricht genau dann um, wenn der Platz nicht reicht.
DASSELBE NOCH EINMAL, am anderen Ende: Bei 1280 px brauchte die Leiste
1235 px (Marke 375 + Bedienelemente 844 + Abstand 16) und hatte 1200.
Auch hier war der Chat-Knopf der Tropfen. Sie brach um, sobald das
Skript die Bedienelemente eingehaengt hatte, und schob die ganze Seite
52 px nach unten -- CLS 0,94. Jetzt gibt die MARKE nach (sie darf
gekuerzt werden, ein Knopf nicht), und umgebrochen wird nur noch auf
sehr schmalen Geraeten.
WEITERE ECHTE FUNDE:
* /workspace/api/chat/ungelesen wurde auf JEDER Seite ZWEIMAL geholt:
einmal beim Laden, einmal Millisekunden spaeter beim Aufgehen des
Ereignisstroms. Der 'open'-Zuhoerer war fuer Wiederverbindungen
gedacht und feuerte auch beim ersten Mal.
* Drei Kacheln teilten sich einen Farbton mit einer anderen (18 Farben
auf 21 Kacheln). Filipe wollte ausdruecklich, dass jede ihre eigene
hat. Nicht eine Farbe dazuerfunden -- der ganze Farbkreis ist mit
tools/kachel-farben.mjs neu in 21 geteilt; kleinster Abstand zweier
Nachbarn jetzt 120 Grad (vorher 106). Dabei fiel eine feste 16 in
der Mischschleife auf: Bei 18 Kacheln wurden die letzten beiden nie
mitgemischt.
* Der Agentur-Untertitel wurde auf dem Handy abgeschnitten.
* Ein zugeklappter <details>-Kasten (Kalender-Abo) verdeckte einen
Filter-Chip: Chromium versteckt dessen Inhalt ueber
`content-visibility`, nicht ueber `display` -- die Kaesten behalten
eine Groesse. Dieselbe Falle wie bei den Zeitbloecken, dieselbe
Loesung.
UND DREI PRUEFUNGEN, DIE SELBST FALSCH LAGEN:
* "achtzehn Kacheln" stand als feste Zahl im Test. Eine Pruefung, die
bei jeder neuen Kachel rot wird, erzieht dazu, ihr Rotwerden zu
ignorieren. Sie zaehlt jetzt aus bereiche.js.
* "das Licht der Kalender-Kachel (tuerkis) muss mehr Blau als Rot
haben" -- Wissen von aussen, und nach dem Neurechnen war der
Kalender rosa. Gemessen wird jetzt gegen den Ton der Kachel selbst.
* pruef-workspace-umzug meldete sein Ergebnis in eigenen Worten. Der
Gesamtlauf las daraus NULL Pruefungen und schrieb bei JEDEM Lauf ein
FEHL, obwohl alle 31 Punkte bestanden. Ein Fehlalarm, der immer
kommt, macht den einen echten unsichtbar.
Und weil "BUTTON 'Meine Sicht' verdeckt" mich eine Stunde gekostet hat,
sagen pruef-handy und pruef-tempo jetzt DAZU, was verdeckt und was
springt -- mit Elternkette und Koordinaten. Ein Befund, den man nicht
verorten kann, ist ein halber.
NEU: der Kalender zum Abonnieren (Stufe 3.2). Persoenlicher, jederzeit
widerrufbarer Link fuer Google, Apple und Outlook. 36 Pruefungen, die
das ICS ZURUECKLESEN statt es anzusehen -- entfaltet, entschluesselt,
verglichen. Wichtigster Punkt: Der Weg ohne Anmeldung darf nichts
zeigen, was der Weg mit Anmeldung nicht zeigt.
Co-Authored-By: Claude Opus 5 <[email protected]>
432 lines
21 KiB
JavaScript
432 lines
21 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");
|
|
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();
|
|
}
|
|
|
|
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);
|