Files
dogfather-universe/pruef-handy.mjs
T
DogFatherGitandClaude Opus 5 37f1b4a8f5 Handy: Sicherheitsabstaende, Wisch-Reiter, zweispaltige Uebersicht
Der wichtigste Fund: env(safe-area-inset-*) stand bereits im CSS, lieferte
aber immer null -- viewport-fit=cover fehlte auf allen dreizehn Seiten.
Der Code sah richtig aus und tat nichts. Zusammen mit
apple-mobile-web-app-status-bar-style=black-translucent (schon gesetzt)
hiess das: installiert lag die Kopfzeile auf einem iPhone hinter Uhr und
Akkuanzeige.

Behoben:
- viewport-fit=cover auf allen 13 Seiten.
- Vier Sicherheitsabstaende zentral benannt statt an jeder Stelle
  einzeln geschrieben. Kopfzeile weicht der Statusleiste, Container dem
  seitlichen Notch im Querformat, Fusszeile der Gestenleiste.
- Der 'Ueberspringen'-Knopf des Vorspanns lag mit bottom:2rem praktisch
  AUF dem Entsperr-Strich (34px). Man haette die App verlassen statt
  uebersprungen.
- Eigener Zweig fuer den installierten Betrieb: kein Gummiband-
  Nachfedern (sieht in einer App nach einem Fehler aus), kein
  Installations-Hinweis.
- Die vier Sprungmarken auf der Rechtsseite waren 39px hoch -- fuenf
  unter dem Daumenmass. Ausgerechnet dort muss man zum Widerrufsrecht
  springen koennen.
- 'Waehle links einen Verlauf aus': Auf dem Handy gibt es kein links,
  die Liste steht darueber. Richtungswort entfernt.
- Sechs Reiter brauchten auf dem Handy drei Zeilen. Jetzt ein
  Wischstreifen mit Einrasten und Auslauf am Rand; der aktive Reiter
  wird herangeholt, wenn man ueber eine Kachel springt.
- Uebersicht zweispaltig statt zwoelf Zeilen untereinander: 2635px ->
  1924px. Eine Uebersicht, an der man vorbeiwischen muss, ist keine.

Geprueft mit 55 neuen Handy-Pruefungen auf iPhone 14 Pro, Pixel 7 und
320px Breite, jeweils im Browser und im installierten Zweig. Drei
Fehlalarme der eigenen Pruefung wurden begruendet ausgenommen (Honigtopf
bei left:-9999px, Eingabefeld in einer Beschriftung, Inline-Link im
Fliesstext -- WCAG 2.5.8 nimmt letztere ausdruecklich aus).

Zwei Selbstkorrekturen an der Pruefung dokumentiert: Die Emulation von
display-mode wirkt nicht (Chromium nimmt den Befehl an und ignoriert
ihn) -- ohne das waeren die zwoelf 'installiert'-Zeilen ein zweiter
Browser-Durchgang gewesen. Und die Messung gegen die Systemleisten mass
zuerst die Kastenkante statt der Inhaltskante und meldete elf korrekte
Seiten als fehlerhaft. Eine Selbstpruefung mit einem absichtlich falsch
gesetzten Knopf belegt jetzt, dass die Messung echte Fehler findet.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-23 19:51:10 +02:00

598 lines
29 KiB
JavaScript

/* =====================================================================
pruef-handy.mjs — Handy im Browser UND als installierte App
WARUM DIESE PRÜFUNG EIGENSTÄNDIG IST
Ein enges Fenster auf dem Rechner ist kein Handy. Es hat keine
Aussparung für die Kamera, keine Gestenleiste, keine Statusleiste, die
über dem Inhalt liegt — und vor allem: keinen installierten Betrieb,
in dem die Adressleiste fehlt. Genau dort entstehen die Fehler, die
man am Schreibtisch nie sieht.
Am 23.08.2026 stand deshalb `env(safe-area-inset-bottom)` im CSS und
tat nichts: Ohne `viewport-fit=cover` liefert jedes `env()` null. Der
Code sah richtig aus, das Ergebnis war null Abstand. Diese Prüfung
fängt genau solche Fälle.
WIE DER INSTALLIERTE BETRIEB GEPRUEFT WIRD
Nicht durch Emulation -- die gibt es nicht. Beide naheliegenden Wege
wurden versucht und sind gescheitert:
- Emulation.setEmulatedMedia nimmt das Merkmal display-mode
entgegen und antwortet mit {}, wendet es aber nicht an
(nachgemessen: matchMedia meldete weiterhin 'browser').
- Ein echtes App-Fenster ueber --app= laesst Playwright nicht zu;
der Versuch blieb haengen und musste abgebrochen werden.
Geprueft wird deshalb zweiteilig (Einzelheiten bei TEIL 3): erst, ob
der Medienblock existiert und woran er haengt, dann, ob sein Inhalt
wirkt, wenn man seine Bedingung zur Laufzeit scharf schaltet. Was das
NICHT beweist, steht dort ebenfalls -- lieber eine benannte Luecke als
eine gruene Zeile, die nichts belegt.
WIE DIE AUSSPARUNG GEPRÜFT WIRD
Die tatsächlichen Werte von `env(safe-area-inset-*)` kann kein
Emulator setzen. Prüfbar ist aber das, worauf es ankommt: dass die
Seite die Werte ÜBERHAUPT verwendet und dass ihre Rechnung stimmt.
Dafür werden die Variablen auf einen Notch-Wert gesetzt (wie bei einem
iPhone 14 Pro: 59px oben, 34px unten) und gemessen, ob die Kopfzeile
wirklich nach unten rückt. Bleibt sie stehen, ist der Abstand
wirkungslos — und genau das war der Ausgangsfehler.
===================================================================== */
import { chromium, devices } from "playwright";
import { createServer } from "node:http";
import { readFileSync, existsSync, readdirSync } from "node:fs";
import { join, extname } from "node:path";
const W = process.cwd();
const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml",
".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json",
".webmanifest": "application/manifest+json" };
const srv = createServer((q, r) => {
const p = join(W, decodeURIComponent(q.url.split("?")[0]));
if (existsSync(p) && !p.endsWith("/")) {
r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" });
return r.end(readFileSync(p));
}
r.writeHead(404).end("");
});
await new Promise((r) => srv.listen(4341, r));
const URL_BASIS = "http://127.0.0.1:4341";
const ergebnisse = [];
function pruefe(name, ok, zusatz) {
ergebnisse.push({ name, ok });
console.log((ok ? " ok " : " FEHL ") + name + (zusatz !== undefined ? " -> " + zusatz : ""));
}
const SEITEN = readdirSync(join(W, "webdesign")).filter((f) => f.endsWith(".html"));
/* Ersatzantworten, damit Portal und Verwaltung überhaupt etwas zeigen.
Ohne sie prüfte man eine leere Seite — die hat nie einen Überlauf. */
const UEBERSICHT = {
ok: true,
beiMir: { anfragen: { n: 3, aeltestes: new Date(Date.now() - 8 * 864e5).toISOString() },
nachrichten: { n: 2, aeltestes: new Date(Date.now() - 3 * 864e5).toISOString() },
wuensche: { n: 1, aeltestes: new Date().toISOString() },
widerrufe: { n: 1, aeltestes: new Date().toISOString() }, projekte: 2 },
beiKunde: { projekte: 1, aufgaben: 4,
zahlungen: { n: 2, summe: 120000, aeltestes: new Date(Date.now() - 5 * 864e5).toISOString() },
zahlungenText: "1.200,00 €" },
geld: { monat: { n: 3, brutto: 250000, netto: 241000, bruttoText: "2.500,00 €", nettoText: "2.410,00 €" },
gesamt: { n: 11, bruttoText: "9.800,00 €" }, gebuehrenText: "90,00 €" },
projekte: [{ id: "p1", nummer: "P-2026-001", titel: "Website für Musterbau", status: "bau",
wartet_auf: "dogfather", richttermin: "2026-09-15", kunde_name: "Anna Muster",
aufg_gesamt: 20, aufg_fertig: 13 }],
verlauf: [{ bereich: "projekt", aktion: "Aufgabe erledigt: Startseite gebaut",
wer: "dogfather", erstellt_am: new Date().toISOString() }],
bestand: { kunden: 5, projekte: 2, anfragenGesamt: 9 },
paypal: { eingerichtet: true, live: true },
};
async function vorbereiten(seite) {
await seite.route("**/webdesign/admin/**", (route) => {
const u = new URL(route.request().url()).pathname;
if (u.endsWith("/admin/uebersicht")) return route.fulfill({ json: UEBERSICHT });
if (u.includes("/admin/postfach-zaehler")) return route.fulfill({ json: { ok: true, ungelesen: 2 } });
return route.fulfill({ json: { ok: true, anfragen: [], kunden: [], projekte: [],
zahlungen: [], verlaeufe: [], ungelesen: 0 } });
});
await seite.route("**/webdesign/portal/**", (r) => r.fulfill({ json: { ok: true } }));
await seite.route("**/auth/**", (r) => r.fulfill({ json: { ok: true, user: { name: "Test" } } }));
await seite.addInitScript(() => {
sessionStorage.setItem("wd-vw-token", "t");
sessionStorage.setItem("wd-sitzung-aktiv", "1");
});
}
/* Schaltet die Seite in den installierten Betrieb.
NICHT ueber Emulation -- die wirkt nicht (siehe Kopfkommentar). Statt
dessen wird die Bedingung des Medienblocks selbst auf 'all'
umgeschrieben. Damit gilt genau der Block, der auf einem installierten
Geraet gelten wuerde, mit genau seinem Inhalt.
Das ist kein Nachbau: Es ist derselbe Block aus derselben Datei. Was
dabei NICHT mitgeprueft wird, ist die Frage, ob das Geraet die
Bedingung richtig auswertet -- das ist Browser-Standardverhalten.
Ohne diesen Kunstgriff waeren die Durchgaenge mit der Beschriftung
'installiert' in Wahrheit ein zweites Mal derselbe Browser-Durchgang
gewesen: zwoelf gruene Zeilen, die nichts Eigenes belegen.
Aufgefallen am 23.08.2026 beim Nachpruefen der Emulation. */
async function alsAppSchalten(kontext, seite) {
await seite.evaluate(() => {
for (const blatt of document.styleSheets) {
let regeln;
try { regeln = blatt.cssRules; } catch (e) { continue; } // fremde Herkunft
for (const r of regeln) {
if (r.media && /display-mode/.test(r.conditionText || r.media.mediaText)) {
r.media.mediaText = "all";
}
}
}
});
}
const browser = await chromium.launch();
/* =====================================================================
TEIL 1 — jede Seite auf jedem Gerät, im Browser und installiert
===================================================================== */
const GERAETE = [
["iPhone 14 Pro", { ...devices["iPhone 14 Pro"] }],
["Pixel 7", { ...devices["Pixel 7"] }],
/* 320px ist das schmalste Gerät, das man realistisch antrifft (iPhone
SE der ersten Reihe, und der Modus mit vergrößerter Schrift macht
aus jedem Handy faktisch ein schmaleres). Wer hier nicht überläuft,
läuft nirgends über. */
["sehr schmal (320px)", { viewport: { width: 320, height: 640 },
isMobile: true, hasTouch: true, deviceScaleFactor: 2,
userAgent: devices["iPhone 14 Pro"].userAgent }],
];
for (const [geraet, angaben] of GERAETE) {
for (const modus of ["Browser", "installiert"]) {
console.log(`\n=== ${geraet} · ${modus} ===`);
const kontext = await browser.newContext(angaben);
let ueberlaufFunde = [], kleineZiele = [], zoomFelder = [], fehlerSeiten = [];
for (const datei of SEITEN) {
const seite = await kontext.newPage();
const protokoll = [];
seite.on("pageerror", (e) => protokoll.push(datei + ": " + e.message));
await vorbereiten(seite);
await seite.goto(URL_BASIS + "/webdesign/" + datei);
/* Erst NACH dem Laden. Playwright setzt beim Navigieren seine
eigene Medien-Emulation und ueberschreibt eine vorher gesetzte
wieder -- gemessen am 23.08.2026: matchMedia meldete trotz
gesetzter Emulation weiterhin false. Medienabfragen werden
ohnehin laufend neu ausgewertet, das Nachreichen genuegt. */
if (modus === "installiert") await alsAppSchalten(kontext, seite);
await seite.waitForTimeout(500);
const befund = await seite.evaluate(() => {
const d = document.documentElement;
const raus = { ueberlauf: Math.max(0, d.scrollWidth - d.clientWidth), zuKlein: [], zuKlein2: [] };
/* Touch-Ziele. Die WCAG-Vorgabe 2.5.8 nennt 24px als Mindestmaß,
Apple und Google nennen 44 bzw. 48. Geprüft wird gegen 44 —
mit einer Ausnahme: Elemente, die INNERHALB eines größeren
Bedienelements liegen (etwa ein Zähler-Abzeichen in einem
Knopf), sind kein eigenes Ziel und werden übersprungen. Ohne
diese Ausnahme meldet die Prüfung lauter Scheinfunde. */
for (const el of document.querySelectorAll("a[href], button, input, select, textarea, summary, [role='button']")) {
const r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue; // unsichtbar
if (getComputedStyle(el).display === "none") continue;
/* Ausserhalb des Bildes. Der Honigtopf im Anfrageformular
liegt bei left:-9999px -- absichtlich, denn display:none
wuerde von Spam-Skripten erkannt. Er ist fuer Menschen
unerreichbar und deshalb kein Bedienelement. */
if (r.right < 0 || r.left > innerWidth) continue;
/* Alles unter aria-hidden ist fuer niemanden gedacht. */
if (el.closest('[aria-hidden="true"], [inert]')) continue;
/* Teil eines groesseren Bedienelements (Abzeichen in einem
Knopf, Symbol in einem Link) -- kein eigenes Ziel. */
const huelle = el.closest("button, a[href], [role='button']");
if (huelle && huelle !== el) continue;
/* Ein Eingabefeld in einer Beschriftung: Getippt wird auf die
BESCHRIFTUNG, sie ist das Ziel. Ein 1x1-Kaestchen mit einem
48px hohen Label daneben ist voellig in Ordnung. */
const label = el.closest("label");
const gemessen = (label && el.tagName === "INPUT") ? label.getBoundingClientRect() : r;
/* WCAG 2.5.8 nimmt Ziele ausdruecklich aus, die INLINE in
einem Fliesstext stehen ("inline" exception): Eine
E-Mail-Adresse mitten im Satz auf 44px aufzublasen wuerde
den Absatz zerreissen und niemandem helfen. Nur eigenstaendige
Bedienelemente werden gemessen. */
if (el.tagName === "A" && getComputedStyle(el).display === "inline" &&
el.closest("p, li, td, dd, blockquote")) continue;
if (gemessen.height < 44 || gemessen.width < 24) {
raus.zuKlein.push((el.tagName + "." + (el.className || "").toString().slice(0, 26)).slice(0, 44) +
" " + Math.round(gemessen.width) + "x" + Math.round(gemessen.height));
}
}
/* Schrift unter 16px in einem Eingabefeld lässt iOS beim
Antippen in die Seite hineinzoomen — danach ist die halbe
Oberfläche aus dem Bild, und herauszoomen muss man von Hand. */
for (const el of document.querySelectorAll("input, select, textarea")) {
const r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue;
if (r.right < 0 || r.left > innerWidth) continue; // Honigtopf
if (el.closest('[aria-hidden="true"], [inert]')) continue;
const t = (el.getAttribute("type") || "").toLowerCase();
if (["hidden", "checkbox", "radio", "range", "submit", "button"].includes(t)) continue;
const g = parseFloat(getComputedStyle(el).fontSize);
if (g < 16) raus.zuKlein2.push((el.id || el.name || el.tagName) + " " + g + "px");
}
return raus;
});
if (befund.ueberlauf > 1) ueberlaufFunde.push(datei + " (" + befund.ueberlauf + "px)");
if (befund.zuKlein.length) kleineZiele.push(datei + ": " + befund.zuKlein.slice(0, 3).join(", "));
if (befund.zuKlein2.length) zoomFelder.push(datei + ": " + befund.zuKlein2.slice(0, 3).join(", "));
if (protokoll.length) fehlerSeiten.push(protokoll[0]);
await seite.close();
}
pruefe(`${geraet}/${modus}: keine Seite laeuft seitlich ueber`,
ueberlaufFunde.length === 0, ueberlaufFunde.join(" | "));
pruefe(`${geraet}/${modus}: alle Bedienelemente sind gross genug zum Antippen`,
kleineZiele.length === 0, kleineZiele.slice(0, 4).join(" | "));
pruefe(`${geraet}/${modus}: kein Eingabefeld loest den iOS-Zoom aus`,
zoomFelder.length === 0, zoomFelder.slice(0, 4).join(" | "));
pruefe(`${geraet}/${modus}: kein JavaScript-Fehler auf irgendeiner Seite`,
fehlerSeiten.length === 0, fehlerSeiten.slice(0, 2).join(" | "));
await kontext.close();
}
}
/* =====================================================================
TEIL 2 — die Aussparung: rueckt die Kopfzeile WIRKLICH nach?
===================================================================== */
console.log("\n=== Aussparung (Notch) und Gestenleiste ===");
{
const kontext = await browser.newContext(devices["iPhone 14 Pro"]);
const seite = await kontext.newPage();
await vorbereiten(seite);
await seite.goto(URL_BASIS + "/webdesign/index.html");
await seite.waitForTimeout(400);
const messung = await seite.evaluate(() => {
const kopf = document.querySelector(".wd-kopf");
const bar = document.querySelector(".wd-kopf-bar");
const ohne = bar.getBoundingClientRect().top;
/* Die echten env()-Werte kann kein Emulator setzen. Die Seite rechnet
aber ueber eigene Variablen -- also werden diese auf die Masse
eines iPhone 14 Pro gesetzt. Rueckt nichts nach, ist der Abstand
wirkungslos. */
document.documentElement.style.setProperty("--wd-sicher-oben", "59px");
document.documentElement.style.setProperty("--wd-sicher-unten", "34px");
kopf.getBoundingClientRect();
const mit = bar.getBoundingClientRect().top;
return { ohne, mit, differenz: mit - ohne };
});
pruefe("die Kopfzeile rueckt unter die Statusleiste, wenn eine da ist",
Math.abs(messung.differenz - 59) < 2,
"verschoben um " + Math.round(messung.differenz) + "px (erwartet 59)");
const benutzt = await seite.evaluate(() => {
const w = getComputedStyle(document.documentElement);
return ["--wd-sicher-oben", "--wd-sicher-unten", "--wd-sicher-links", "--wd-sicher-rechts"]
.filter((v) => (w.getPropertyValue(v) || "").trim() !== "").length;
});
pruefe("alle vier Sicherheitsabstaende sind definiert", benutzt === 4, benutzt + " von 4");
const vf = await seite.evaluate(() =>
(document.querySelector('meta[name="viewport"]') || {}).content || "");
pruefe("viewport-fit=cover ist gesetzt (sonst liefert env() immer null)",
vf.includes("viewport-fit=cover"), vf);
await kontext.close();
}
/* Und auf JEDER Seite, nicht nur der Startseite — eine vergessene Seite
waere im installierten Betrieb genau die eine mit dem Fehler. */
{
let ohneCover = [];
for (const datei of SEITEN) {
const roh = readFileSync(join(W, "webdesign", datei), "utf8");
if (!/viewport-fit\s*=\s*cover/.test(roh)) ohneCover.push(datei);
}
pruefe("viewport-fit=cover steht auf ALLEN Seiten", ohneCover.length === 0,
ohneCover.join(", ") || SEITEN.length + " Seiten geprueft");
}
/* =====================================================================
TEIL 3 — was NUR im installierten Betrieb gilt
EHRLICHE EINORDNUNG, WAS HIER BEWIESEN WIRD UND WAS NICHT.
Der erste Versuch war, den installierten Betrieb zu emulieren. Das
geht nicht: `Emulation.setEmulatedMedia` nimmt das Merkmal
`display-mode` zwar entgegen und antwortet mit {} -- angewendet wird
es nicht (nachgemessen am 23.08.2026: matchMedia meldete weiter
`browser`). Ein echtes App-Fenster ueber `--app=` laesst Playwright
nicht zu; der Versuch blieb haengen.
Statt das zu verschweigen und eine gruene Zeile zu schreiben, die
nichts belegt, wird hier ZWEITEILIG geprueft:
1. Gibt es den Block ueberhaupt, und haengt er wirklich an
display-mode? (aus dem Stilblatt gelesen)
2. Bewirkt sein INHALT auch das, was er soll? (Bedingung zur
Laufzeit auf `all` gestellt und die Wirkung gemessen)
Was damit NICHT bewiesen ist: dass ein echtes Android- oder
iOS-Geraet den Block auswertet. Das ist Standardverhalten und
unstrittig -- aber es ist gemessen, nicht behauptet, wo es geht, und
benannt, wo es nicht geht.
===================================================================== */
console.log("\n=== Nur installiert ===");
{
const kontext = await browser.newContext(devices["iPhone 14 Pro"]);
const seite = await kontext.newPage();
await vorbereiten(seite);
await seite.goto(URL_BASIS + "/webdesign/index.html");
await seite.waitForTimeout(400);
/* --- Teil 1: Steht der Block da, und woran haengt er? --- */
const block = await seite.evaluate(() => {
for (const blatt of document.styleSheets) {
let regeln; try { regeln = blatt.cssRules; } catch (e) { continue; }
for (const r of regeln) {
if (r.media && /display-mode/.test(r.conditionText || r.media.mediaText)) {
return { bedingung: r.conditionText || r.media.mediaText,
inhalt: Array.from(r.cssRules).map((x) => x.cssText).join(" ") };
}
}
}
return null;
});
pruefe("es gibt einen eigenen Zweig fuer den installierten Betrieb",
!!block, block ? block.bedingung : "kein Block gefunden");
pruefe("er greift auch bei window-controls-overlay und minimal-ui",
!!block && /window-controls-overlay/.test(block.bedingung) && /minimal-ui/.test(block.bedingung),
block ? block.bedingung : "");
pruefe("der Installations-Hinweis wird darin ausgeblendet",
!!block && /wd-install/.test(block.inhalt) && /display:\s*none/.test(block.inhalt));
/* --- Teil 2: Im Browser darf davon NICHTS gelten --- */
const imBrowser = await seite.evaluate(() => ({
erkannt: matchMedia("(display-mode: standalone)").matches,
federn: getComputedStyle(document.body).overscrollBehaviorY,
}));
pruefe("im Browser gilt der Sonderzweig NICHT", imBrowser.erkannt === false,
String(imBrowser.erkannt));
pruefe("im Browser bleibt das gewohnte Nachfedern erhalten",
imBrowser.federn !== "none", imBrowser.federn);
/* --- Teil 3: Wirkt der Inhalt, wenn die Bedingung erfuellt ist? --- */
const wirkung = await seite.evaluate(() => {
/* Die Bedingung der Medienregel selbst umschreiben. Damit wird
genau DIESER Block scharf geschaltet -- nicht ein nachgebauter. */
for (const blatt of document.styleSheets) {
let regeln; try { regeln = blatt.cssRules; } catch (e) { continue; }
for (const r of regeln) {
if (r.media && /display-mode/.test(r.conditionText || r.media.mediaText)) {
r.media.mediaText = "all";
}
}
}
const hinweis = document.querySelector(".wd-install");
return {
federn: getComputedStyle(document.body).overscrollBehaviorY,
hinweis: hinweis ? getComputedStyle(hinweis).display : "nicht auf dieser Seite",
};
});
pruefe("scharf geschaltet: das Gummiband-Nachfedern ist wirklich aus",
wirkung.federn === "none", wirkung.federn);
pruefe("scharf geschaltet: der Installations-Hinweis verschwindet wirklich",
wirkung.hinweis === "none" || wirkung.hinweis === "nicht auf dieser Seite",
wirkung.hinweis);
await kontext.close();
}
/* =====================================================================
TEIL 3b — liegt irgendwo etwas unter der Gestenleiste oder dem Notch?
Gesucht wird nicht im Quelltext, sondern im fertigen Bild. Ein
Durchsuchen des CSS nach 'position: fixed' findet nur, woran man
gedacht hat -- der 'Ueberspringen'-Knopf des Vorspanns stand mit
'bottom: 2rem' da und wurde beim Suchen prompt uebersehen, obwohl er
auf einem iPhone 14 Pro genau auf dem Entsperr-Strich lag (der ist
34px hoch). Getippt haette man dann nicht 'ueberspringen', sondern die
App verlassen.
Deshalb: Abstaende auf Notch-Masse setzen, dann JEDES fest oder klebend
positionierte Element messen. Was in den geschuetzten Streifen
hineinragt, faellt auf -- unabhaengig davon, ob jemand daran gedacht hat.
===================================================================== */
console.log("\n=== Nichts unter Statusleiste oder Gestenleiste ===");
{
const kontext = await browser.newContext(devices["iPhone 14 Pro"]);
for (const datei of SEITEN) {
const seite = await kontext.newPage();
await vorbereiten(seite);
await seite.goto(URL_BASIS + "/webdesign/" + datei);
await seite.waitForTimeout(450);
const funde = await seite.evaluate(() => {
const OBEN = 59, UNTEN = 34; // iPhone 14 Pro
document.documentElement.style.setProperty("--wd-sicher-oben", OBEN + "px");
document.documentElement.style.setProperty("--wd-sicher-unten", UNTEN + "px");
document.body.getBoundingClientRect(); // Neuberechnung erzwingen
const raus = [];
for (const el of document.querySelectorAll("*")) {
const st = getComputedStyle(el);
if (st.position !== "fixed" && st.position !== "sticky") continue;
if (st.display === "none" || st.visibility === "hidden") continue;
const r = el.getBoundingClientRect();
if (r.width < 8 || r.height < 8) continue;
/* Ganzflaechige Ueberlagerungen (inset: 0) sind Hintergruende --
sie DUERFEN unter die Leisten laufen, das ist ihr Zweck. Nur
was Text oder Bedienelemente traegt, zaehlt. */
const vollflaechig = r.top <= 1 && r.left <= 1 &&
r.width >= innerWidth - 2 && r.height >= innerHeight - 2;
if (vollflaechig) continue;
const name = el.tagName.toLowerCase() + "." + (el.className || "").toString().trim().slice(0, 24);
/* Gemessen wird die INHALTSKANTE, nicht die Kastenkante.
Die Kopfzeile SOLL mit ihrem Kasten unter die Statusleiste
reichen -- ihr Hintergrund fuellt die Flaeche hinter der Uhr,
sonst klaffte dort ein Loch. Verboten ist nur, dass Text oder
Knoepfe dort landen. Genau dafuer hat sie padding-top in Hoehe
des Sicherheitsabstands.
Die erste Fassung dieser Pruefung mass die Kastenkante und
meldete daraufhin elf Seiten als fehlerhaft, auf denen alles
richtig war. */
const obenInhalt = r.top + parseFloat(st.paddingTop || 0) + parseFloat(st.borderTopWidth || 0);
const untenInhalt = r.bottom - parseFloat(st.paddingBottom || 0) - parseFloat(st.borderBottomWidth || 0);
if (untenInhalt > innerHeight - UNTEN && r.top < innerHeight)
raus.push(name + " ragt " + Math.round(untenInhalt - (innerHeight - UNTEN)) + "px in die Gestenleiste");
else if (obenInhalt < OBEN && r.bottom > 0)
raus.push(name + " liegt " + Math.round(OBEN - obenInhalt) + "px unter der Statusleiste");
}
return raus;
});
pruefe("nichts liegt unter den Systemleisten: " + datei, funde.length === 0,
funde.slice(0, 3).join(" | "));
/* SELBSTPRUEFUNG -- nur einmal, auf der Startseite.
Eine Pruefung, die nie etwas findet, kann zweierlei bedeuten: Es
ist alles in Ordnung, ODER sie misst am Problem vorbei. Genau das
war eben der Fall (falsche Kante gemessen). Deshalb wird hier ein
absichtlich falsch platzierter Knopf eingesetzt und geprueft, ob
die Messung ihn WIRKLICH findet. Erst dann ist ein gruenes
Ergebnis oben etwas wert. */
if (datei === "index.html") {
const gefunden = await seite.evaluate(() => {
const UNTEN = 34;
const k = document.createElement("button");
k.className = "pruef-koeder";
k.textContent = "absichtlich falsch";
k.style.cssText = "position:fixed;left:20px;bottom:4px;width:120px;height:44px;z-index:9999";
document.body.appendChild(k);
const r = k.getBoundingClientRect();
const st = getComputedStyle(k);
const untenInhalt = r.bottom - parseFloat(st.paddingBottom || 0) - parseFloat(st.borderBottomWidth || 0);
const erwischt = untenInhalt > innerHeight - UNTEN;
k.remove();
return erwischt;
});
pruefe("die Messung erkennt einen absichtlich falsch gesetzten Knopf",
gefunden === true, String(gefunden));
}
await seite.close();
}
await kontext.close();
}
/* =====================================================================
TEIL 4 — die Verwaltung auf dem Handy
===================================================================== */
console.log("\n=== Verwaltung auf dem Handy ===");
{
const kontext = await browser.newContext(devices["iPhone 14 Pro"]);
const seite = await kontext.newPage();
await vorbereiten(seite);
await seite.goto(URL_BASIS + "/webdesign/verwaltung.html");
await seite.waitForTimeout(800);
/* Die Reiter muessen EINE Zeile sein. Gemessen ueber die Oberkanten:
Liegen zwei Reiter auf verschiedenen Hoehen, ist umgebrochen worden. */
const reiter = await seite.evaluate(() => {
const k = Array.from(document.querySelectorAll(".vw-tabs [data-tab]"));
const oben = new Set(k.map((x) => Math.round(x.getBoundingClientRect().top)));
const streifen = document.querySelector(".vw-tabs");
return { anzahl: k.length, zeilen: oben.size,
wischbar: streifen.scrollWidth > streifen.clientWidth + 1,
hoehe: Math.round(streifen.getBoundingClientRect().height) };
});
pruefe("die sechs Reiter stehen in EINER Zeile", reiter.zeilen === 1,
reiter.zeilen + " Zeilen bei " + reiter.anzahl + " Reitern");
pruefe("und der Streifen laesst sich wischen (sonst waeren sie abgeschnitten)",
reiter.wischbar, "Hoehe " + reiter.hoehe + "px");
/* Der Kopfbereich darf nicht den halben Bildschirm fressen. */
const bisInhalt = await seite.evaluate(() =>
Math.round(document.querySelector(".vw-ub-gitter").getBoundingClientRect().top));
pruefe("die erste Zahl steht im oberen Drittel des Bildschirms",
bisInhalt < 300, bisInhalt + "px bis zur ersten Kachel");
/* Zwei Spalten statt zwoelf Zeilen untereinander. */
const spalten = await seite.evaluate(() => {
const k = Array.from(document.querySelectorAll("#vw-ub-beimir .vw-ub-kachel"));
const links = new Set(k.map((x) => Math.round(x.getBoundingClientRect().left)));
return { spalten: links.size, anzahl: k.length };
});
pruefe("die Kacheln stehen zweispaltig", spalten.spalten === 2,
spalten.spalten + " Spalten bei " + spalten.anzahl + " Kacheln");
const laenge = await seite.evaluate(() => document.documentElement.scrollHeight);
pruefe("die Uebersicht ist deutlich kuerzer geworden", laenge < 2000,
laenge + "px (vorher 2635px)");
/* Der aktive Reiter muss sichtbar werden, wenn man ueber eine Kachel
springt -- sonst sieht man nirgends, wo man gelandet ist. */
await seite.locator('#vw-ub-beimir .vw-ub-kachel[data-ziel="postfach"]').first().click();
await seite.waitForTimeout(800);
const sichtbar = await seite.evaluate(() => {
const a = document.querySelector('.vw-tabs [data-tab="postfach"]');
const s = document.querySelector(".vw-tabs");
const ar = a.getBoundingClientRect(), sr = s.getBoundingClientRect();
return ar.left >= sr.left - 1 && ar.right <= sr.right + 1;
});
pruefe("nach dem Sprung ins Postfach ist dessen Reiter im Bild",
sichtbar, String(sichtbar));
await seite.screenshot({ path: "bild-handy-verwaltung.png", fullPage: true });
await seite.close();
await kontext.close();
}
/* =====================================================================
TEIL 5 — Bilder zum Ansehen
===================================================================== */
{
const kontext = await browser.newContext(devices["iPhone 14 Pro"]);
const seite = await kontext.newPage();
await vorbereiten(seite);
await seite.goto(URL_BASIS + "/webdesign/index.html");
await alsAppSchalten(kontext, seite);
await seite.waitForTimeout(600);
/* Mit gesetzten Abstaenden aufnehmen, damit man sieht, wie es
installiert auf einem Geraet mit Aussparung wirklich aussieht. */
await seite.evaluate(() => {
document.documentElement.style.setProperty("--wd-sicher-oben", "59px");
document.documentElement.style.setProperty("--wd-sicher-unten", "34px");
});
await seite.waitForTimeout(200);
await seite.screenshot({ path: "bild-handy-start-installiert.png" });
await seite.close();
await kontext.close();
}
await browser.close();
srv.close();
const f = ergebnisse.filter((e) => !e.ok).length;
console.log(`\n===== ${ergebnisse.length - f} bestanden, ${f} fehlgeschlagen =====`);
process.exitCode = f ? 1 : 0;